├── misc └── KatokenMusicPlay.lua ├── README.md ├── LICENSE ├── gb2midi.lua ├── pce2midi.lua ├── nes2midi.lua └── emu2midi.lua /misc/KatokenMusicPlay.lua: -------------------------------------------------------------------------------- 1 | -- PCE Katochan & Kenchan Music Player 2 | memory.writebyte(0x1F00F1,0x01) -- music number 3 | memory.writebyte(0x1F1E00,0x00) -- unpause 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | emu2midi-lua 2 | ============ 3 | 4 | EmuLua scripts for recording retro game sound to MIDI file and/or [FlMML](http://flmml.codeplex.com/). 5 | 6 | Lineups 7 | ------- 8 | 9 | - gb2midi: Gameboy sound. Use [VBA-RR](https://code.google.com/p/vba-rerecording/) *V23* (V24 does not work) to run. 10 | - nes2midi: NES sound (no extra chip support). Use [FCEUX](http://www.fceux.com) to run. 11 | - pce2midi: PC-Engine (TurboGrafx-16) sound. Use [PCEjin](https://code.google.com/p/pcejin/) to run. 12 | 13 | Limitations 14 | ----------- 15 | 16 | - Time resolution of conversion is 1/60 seconds. Some sound effects may sound strange because of it. 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 gocha / ごちゃ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /gb2midi.lua: -------------------------------------------------------------------------------- 1 | -- Note: do not use vba-rr v24, it doesn't work on GB games. use v23 instead. 2 | require("emu2midi") 3 | 4 | function GBSoundWriter() 5 | local self = VGMSoundWriter() 6 | 7 | -- functions in base class 8 | self.base = ToFunctionTable(self); 9 | 10 | -- channel type list 11 | self.CHANNEL_TYPE = { 12 | SQUARE = "square"; 13 | WAVEMEMORY = "wavememory"; 14 | NOISE = "noise"; 15 | }; 16 | 17 | -- pseudo patch number for noise 18 | self.NOISE_PATCH_NUMBER = { 19 | LONG = 0; 20 | SHORT = 1; 21 | }; 22 | 23 | self.FRAMERATE = 16777216 / 280896; 24 | 25 | -- reset current logging state 26 | self.clear = function(self) 27 | self.base.clear(self) 28 | 29 | -- tempo (fixed value) 30 | local bpm = self.FRAMERATE 31 | table.insert(self.scoreGlobal, { 'set_tempo', 0, math.floor(60000000 / bpm) }) 32 | end; 33 | 34 | -- get MIDI TPQN (integer) 35 | self.getTPQN = function(self) 36 | return 60 37 | end; 38 | 39 | -- event conversion: convert patch event for MIDI 40 | -- @param source 'patch_change' event 41 | self.eventPatchToMidi = function(self, event) 42 | return { { 'patch_change', event[2], event[3], event[4] } } 43 | end; 44 | 45 | -- get FlMML patch command 46 | -- @param string patch type (wavememory, dpcm, etc.) 47 | -- @param number patch number 48 | -- @return string patch mml text 49 | self.getFlMMLPatchCmd = function(self, patchType, patchNumber) 50 | if patchType == self.CHANNEL_TYPE.SQUARE then 51 | if patchNumber >= 0 and patchNumber <= 3 then 52 | local dutyTable = { 1, 2, 4, 6 } 53 | return string.format("@5@W%d", dutyTable[1 + patchNumber]) 54 | else 55 | error(string.format("Unknown patch number '%d' for '%s'", patchNumber, patchType)) 56 | end 57 | elseif patchType == self.CHANNEL_TYPE.WAVEMEMORY then 58 | return string.format("@13-%d", patchNumber) 59 | elseif patchType == self.CHANNEL_TYPE.NOISE then 60 | if patchNumber == self.NOISE_PATCH_NUMBER.LONG then 61 | return "@11" 62 | elseif patchNumber == self.NOISE_PATCH_NUMBER.SHORT then 63 | return "@12" 64 | else 65 | error(string.format("Unknown patch number '%d' for '%s'", patchNumber, patchType)) 66 | end 67 | else 68 | error(string.format("Unknown patch type '%s'", patchType)) 69 | end 70 | end; 71 | 72 | -- get FlMML waveform definition MML 73 | -- @return string waveform define mml 74 | self.getFlMMLWaveformDef = function(self) 75 | local mml = "" 76 | for waveChannelType, waveList in pairs(self.waveformList) do 77 | for waveIndex, waveValue in ipairs(waveList) do 78 | if waveChannelType == self.CHANNEL_TYPE.WAVEMEMORY then 79 | mml = mml .. string.format("#WAV13 %d,%s\n", waveIndex - 1, waveValue) 80 | else 81 | error(string.format("Unknown patch type '%s'", waveChannelType)) 82 | end 83 | end 84 | end 85 | return mml 86 | end; 87 | 88 | self:clear() 89 | return self 90 | end 91 | 92 | local writer = GBSoundWriter() 93 | 94 | emu.registerafter(function() 95 | local ch = {} 96 | local channels = {} 97 | local snd = sound.get() 98 | 99 | ch = snd.square1 100 | ch.type = writer.CHANNEL_TYPE.SQUARE 101 | ch.patch = ch.duty 102 | table.insert(channels, ch) 103 | 104 | ch = snd.square2 105 | ch.type = writer.CHANNEL_TYPE.SQUARE 106 | ch.patch = ch.duty 107 | table.insert(channels, ch) 108 | 109 | ch = snd.wavememory 110 | ch.type = writer.CHANNEL_TYPE.WAVEMEMORY 111 | ch.patch = writer.bytestohex(ch.waveform) 112 | table.insert(channels, ch) 113 | 114 | ch = snd.noise 115 | ch.type = writer.CHANNEL_TYPE.NOISE 116 | ch.midikey = writer.gbNoiseFreqRegToNote(ch.regs.frequency) 117 | ch.patch = (ch.short and writer.NOISE_PATCH_NUMBER.SHORT or writer.NOISE_PATCH_NUMBER.LONG) 118 | table.insert(channels, ch) 119 | 120 | writer:write(channels) 121 | end) 122 | 123 | _registerexit_firstrun = true 124 | emu.registerexit(function() 125 | if _registerexit_firstrun then 126 | -- vba: without this, we will get an infinite loop on error 127 | _registerexit_firstrun = false 128 | 129 | writer:writeTextFile("testVGM.txt") 130 | writer:writeMidiFile("testVGM.mid") 131 | writer:writeFlMMLFile("testVGM.mml") 132 | end 133 | end) 134 | -------------------------------------------------------------------------------- /pce2midi.lua: -------------------------------------------------------------------------------- 1 | require("emu2midi") 2 | 3 | PCE_PSG_BASE = (21477272 + (72/99)) / 3 / 2 -- 72/99=0.7272... 4 | function PCESoundWriter() 5 | local self = VGMSoundWriter() 6 | 7 | -- functions in base class 8 | self.base = ToFunctionTable(self); 9 | 10 | -- channel type list 11 | self.CHANNEL_TYPE = { 12 | WAVEMEMORY = "wavememory"; 13 | NOISE = "noise"; 14 | }; 15 | 16 | self.FRAMERATE = PCE_PSG_BASE * 2 / 455 / 263; 17 | 18 | -- reset current logging state 19 | self.clear = function(self) 20 | self.base.clear(self) 21 | 22 | -- tempo (fixed value) 23 | local bpm = self.FRAMERATE 24 | table.insert(self.scoreGlobal, { 'set_tempo', 0, math.floor(60000000 / bpm) }) 25 | end; 26 | 27 | -- get MIDI TPQN (integer) 28 | self.getTPQN = function(self) 29 | return 60 30 | end; 31 | 32 | -- event conversion: convert patch event for MIDI 33 | -- @param source 'patch_change' event 34 | self.eventPatchToMidi = function(self, event) 35 | return { { 'patch_change', event[2], event[3], event[4] } } 36 | end; 37 | 38 | -- static pceNoiseRegToFreq 39 | -- @param number noise frequency register value (0-31) 40 | -- @return number noise frequency [Hz] 41 | self.pceNoiseRegToFreq = function(reg) 42 | assert(reg >= 0 and reg <= 0x1f) 43 | local freqDiv = 0x1f - reg 44 | if freqDiv == 0 then 45 | freqDiv = 0.5 46 | end 47 | return PCE_PSG_BASE / 64 / freqDiv 48 | end; 49 | 50 | -- convert 5bit wave to 8bit wave 51 | self.byte5bitTo8bit = function(bytestring) 52 | if bytestring == nil then 53 | return nil 54 | end 55 | 56 | local str = "" 57 | assert(#bytestring == 32) 58 | for i = 1, #bytestring do 59 | local raw5 = string.byte(bytestring, i) 60 | assert(raw5 >= 0 and raw5 <= 31) 61 | local raw8 = (raw5 * 8) -- + math.floor(raw5 / 4) 62 | str = str .. string.char(raw8) 63 | end 64 | return str 65 | end; 66 | 67 | -- get FlMML patch command 68 | -- @param string patch type (wavememory, dpcm, etc.) 69 | -- @param number patch number 70 | -- @return string patch mml text 71 | self.getFlMMLPatchCmd = function(self, patchType, patchNumber) 72 | if patchType == self.CHANNEL_TYPE.WAVEMEMORY then 73 | return string.format("@13-%d", patchNumber) 74 | elseif patchType == self.CHANNEL_TYPE.NOISE then 75 | return "@11" 76 | else 77 | error(string.format("Unknown patch type '%s'", patchType)) 78 | end 79 | end; 80 | 81 | -- get FlMML waveform definition MML 82 | -- @return string waveform define mml 83 | self.getFlMMLWaveformDef = function(self) 84 | local mml = "" 85 | for waveChannelType, waveList in pairs(self.waveformList) do 86 | for waveIndex, waveValue in ipairs(waveList) do 87 | if waveChannelType == self.CHANNEL_TYPE.WAVEMEMORY then 88 | mml = mml .. string.format("#WAV13 %d,%s\n", waveIndex - 1, waveValue) 89 | else 90 | error(string.format("Unknown patch type '%s'", waveChannelType)) 91 | end 92 | end 93 | end 94 | return mml 95 | end; 96 | 97 | -- get FlMML tuning for each patches 98 | -- @param number origNoteNumber input note number 99 | -- @param string patchType patch type (square, noise, etc.) 100 | -- @return number output note number 101 | self.getFlMMLNoteNumber = function(self, origNoteNumber, patchType) 102 | if patchType == self.CHANNEL_TYPE.NOISE then 103 | -- convert to gameboy noise 104 | return self.gbNoiseFreqToNote(self.pceNoiseRegToFreq(origNoteNumber)) 105 | else 106 | return origNoteNumber 107 | end 108 | end; 109 | 110 | self:clear() 111 | return self 112 | end 113 | 114 | local writer = PCESoundWriter() 115 | 116 | emu.registerafter(function() 117 | local ch = {} 118 | local channels = {} 119 | local snd = sound.get() 120 | 121 | for chIndex = 1, #snd.channel do 122 | ch = snd.channel[chIndex] 123 | if ch.noise then 124 | ch.type = writer.CHANNEL_TYPE.NOISE 125 | ch.midikey = ch.noise 126 | ch.patch = 127 127 | else 128 | ch.type = writer.CHANNEL_TYPE.WAVEMEMORY 129 | -- TODO: handle ch.dda 130 | ch.patch = writer.bytestohex(writer.byte5bitTo8bit(ch.waveform)) 131 | end 132 | 133 | --ch.volume = math.pow(10.0, (-1.5 * (0x1f - ch.regs.volume)) / 20.0) -- envelope only 134 | --ch.volume = (math.pow(10.0, (-3.0 * (0xf - ch.regs.leftvolume)) / 20.0) + math.pow(10.0, (-3.0 * (0xf - ch.regs.rightvolume)) / 20.0)) / 2 -- w/o envelope 135 | if ch.regs.volume == 0 then 136 | ch.volume = 0 137 | end 138 | 139 | table.insert(channels, ch) 140 | end 141 | 142 | writer:write(channels) 143 | end) 144 | 145 | emu.registerexit(function() 146 | writer:writeTextFile("testVGM.txt") 147 | writer:writeMidiFile("testVGM.mid") 148 | writer:writeFlMMLFile("testVGM.mml") 149 | end) 150 | -------------------------------------------------------------------------------- /nes2midi.lua: -------------------------------------------------------------------------------- 1 | require("emu2midi") 2 | local base64 = require("base64") -- http://www.tecgraf.puc-rio.br/~lhf/ftp/lua/#lbase64 (visit LuaForWindows for Windows installation) 3 | 4 | function NESSoundWriter() 5 | local self = VGMSoundWriter() 6 | 7 | -- functions in base class 8 | self.base = ToFunctionTable(self); 9 | 10 | -- channel type list 11 | self.CHANNEL_TYPE = { 12 | SQUARE = "square"; 13 | TRIANGLE = "triangle"; 14 | NOISE = "noise"; 15 | DPCM = "dpcm"; 16 | }; 17 | 18 | -- pseudo patch number for noise 19 | self.NOISE_PATCH_NUMBER = { 20 | LONG = 0; 21 | SHORT = 1; 22 | }; 23 | 24 | self.FRAMERATE = 39375000 / 11 * 3 / 2 / 341 / 262; 25 | 26 | -- reset current logging state 27 | self.clear = function(self) 28 | self.base.clear(self) 29 | 30 | -- tempo (fixed value) 31 | local bpm = self.FRAMERATE 32 | table.insert(self.scoreGlobal, { 'set_tempo', 0, math.floor(60000000 / bpm) }) 33 | end; 34 | 35 | -- get MIDI TPQN (integer) 36 | self.getTPQN = function(self) 37 | return 60 38 | end; 39 | 40 | -- event conversion: convert patch event for MIDI 41 | -- @param source 'patch_change' event 42 | self.eventPatchToMidi = function(self, event) 43 | return { { 'patch_change', event[2], event[3], event[4] } } 44 | end; 45 | 46 | -- get FlMML patch command 47 | -- @param string patch type (wavememory, dpcm, etc.) 48 | -- @param number patch number 49 | -- @return string patch mml text 50 | self.getFlMMLPatchCmd = function(self, patchType, patchNumber) 51 | if patchType == self.CHANNEL_TYPE.SQUARE then 52 | if patchNumber >= 0 and patchNumber <= 3 then 53 | local dutyTable = { 1, 2, 4, 6 } 54 | return string.format("@5@W%d", dutyTable[1 + patchNumber]) 55 | else 56 | error(string.format("Unknown patch number '%d' for '%s'", patchNumber, patchType)) 57 | end 58 | elseif patchType == self.CHANNEL_TYPE.TRIANGLE then 59 | return "@6-1" 60 | elseif patchType == self.CHANNEL_TYPE.NOISE then 61 | if patchNumber == self.NOISE_PATCH_NUMBER.LONG then 62 | return "@7" 63 | elseif patchNumber == self.NOISE_PATCH_NUMBER.SHORT then 64 | return "@8" 65 | else 66 | error(string.format("Unknown patch number '%d' for '%s'", patchNumber, patchType)) 67 | end 68 | elseif patchType == self.CHANNEL_TYPE.DPCM then 69 | return string.format("@9-%d", patchNumber) 70 | else 71 | error(string.format("Unknown patch type '%s'", patchType)) 72 | end 73 | end; 74 | 75 | -- get FlMML waveform definition MML 76 | -- @return string waveform define mml 77 | self.getFlMMLWaveformDef = function(self) 78 | local mml = "" 79 | for waveChannelType, waveList in pairs(self.waveformList) do 80 | for waveIndex, waveValue in ipairs(waveList) do 81 | if waveChannelType == self.CHANNEL_TYPE.DPCM then 82 | mml = mml .. string.format("#WAV9 %d,%s\n", waveIndex - 1, waveValue) 83 | else 84 | error(string.format("Unknown patch type '%s'", waveChannelType)) 85 | end 86 | end 87 | end 88 | return mml 89 | end; 90 | 91 | self:clear() 92 | return self 93 | end 94 | 95 | local writer = NESSoundWriter() 96 | 97 | emu.registerafter(function() 98 | local ch = {} 99 | local channels = {} 100 | local snd = sound.get() 101 | 102 | ch = snd.rp2a03.square1 103 | ch.type = writer.CHANNEL_TYPE.SQUARE 104 | ch.patch = ch.duty 105 | table.insert(channels, ch) 106 | 107 | ch = snd.rp2a03.square2 108 | ch.type = writer.CHANNEL_TYPE.SQUARE 109 | ch.patch = ch.duty 110 | table.insert(channels, ch) 111 | 112 | ch = snd.rp2a03.triangle 113 | ch.type = writer.CHANNEL_TYPE.TRIANGLE 114 | ch.patch = 0 115 | if ch.regs.frequency == 0 then -- freq reg = 0 (pseudo mute) 116 | ch.midikey = 0 117 | ch.volume = 0 118 | end 119 | table.insert(channels, ch) 120 | 121 | ch = snd.rp2a03.noise 122 | ch.type = writer.CHANNEL_TYPE.NOISE 123 | ch.midikey = ch.regs.frequency 124 | ch.patch = (ch.short and writer.NOISE_PATCH_NUMBER.SHORT or writer.NOISE_PATCH_NUMBER.LONG) 125 | table.insert(channels, ch) 126 | 127 | ch = snd.rp2a03.dpcm 128 | ch.type = writer.CHANNEL_TYPE.DPCM 129 | ch.midikey = ch.regs.frequency 130 | ch.patch = nil 131 | if ch.volume ~= 0 then 132 | ch.patch = string.format("%d,%d,%s", ch.dmcseed, ch.dmcloop and 1 or 0, base64.encode(memory.readbyterange(ch.dmcaddress, ch.dmcsize))) 133 | end 134 | table.insert(channels, ch) 135 | 136 | writer:write(channels) 137 | end) 138 | 139 | _registerexit_firstrun = true 140 | emu.registerexit(function() 141 | if _registerexit_firstrun then 142 | -- fceux: without this, we will get an error message for several times 143 | _registerexit_firstrun = false 144 | 145 | writer:writeTextFile("testVGM.txt") 146 | writer:writeMidiFile("testVGM.mid") 147 | writer:writeFlMMLFile("testVGM.mml") 148 | end 149 | end) 150 | -------------------------------------------------------------------------------- /emu2midi.lua: -------------------------------------------------------------------------------- 1 | -- Generic MIDI/FlMML recorder abstruct class on EmuLua 2 | -- @author gocha 3 | -- @dependency http://www.pjb.com.au/comp/lua/MIDI.html 4 | 5 | function ToFunctionTable(classObj) 6 | local funcTable = {} 7 | for k, v in pairs(classObj) do 8 | if type(v) == "function" then 9 | funcTable[k] = v 10 | end 11 | end 12 | return funcTable 13 | end 14 | 15 | function VGMSoundWriter() 16 | local self = { 17 | --[[ 18 | class VGMSoundChannel { 19 | -- the following members must be given to write() 20 | number midikey; -- real number, not integer 21 | number volume; -- real number [0.0-1.0] 22 | number panpot; -- real number [0.0-1.0], 0.5 for center 23 | string type; -- type identifier, something like "square", "noise", etc. 24 | object patch; -- patch identifier (number: patch number, string: patch identifier, waveform address for example) 25 | 26 | -- derived classes may have more additional members 27 | }; 28 | ]] 29 | 30 | -- function table for derived class 31 | -- IMPORTANT NOTE: multiple inheritance is not allowed 32 | base = {}; 33 | 34 | -- tick for new event 35 | tick = 0; 36 | 37 | -- VGMSoundChannel[] last sound state, used for logging 38 | lastValue = {}; 39 | 40 | -- waveform list, which is used to record existing waveforms in a song 41 | -- for example: waveformList["dpcm"][1] = "3A00 (value is wavetable identifier, such as waveform address or waveform itself)"; 42 | waveformList = {}; 43 | 44 | -- score[], sound state log for each channels 45 | scoreChannel = {}; 46 | -- score, sound state log for global things (e.g. Master Volume) 47 | scoreGlobal = {}; 48 | 49 | -- pitch bend amount to consider a new note 50 | NOTE_PITCH_THRESHOLD = 0.68; 51 | 52 | -- pitch bend amount to detect unwanted "small pitch change" with a new note 53 | NOTE_PITCH_STRIP_THRESHOLD = 0.0; 54 | 55 | -- volume up amount to consider a new note 56 | NOTE_VOLUME_THRESHOLD = 0.25; 57 | 58 | -- minimal volume (mute very small volume) 59 | VOLUME_MUTE_THRESHOLD = 0.0; 60 | 61 | -- midi note velocity value 62 | NOTE_VELOCITY = 100; 63 | 64 | -- midi pitch bend range value 65 | MIDI_PITCHBEND_RANGE = 12; 66 | 67 | -- midi volume/panpot curve on/off 68 | MIDI_LINEAR_CONVERSION = false; 69 | 70 | -- flmml timebase 71 | FLMML_TPQN = 96; 72 | 73 | -- static math.round 74 | -- http://lua-users.org/wiki/SimpleRound 75 | round = function(num, idp) 76 | local mult = 10^(idp or 0) 77 | return math.floor(num * mult + 0.5) / mult 78 | end; 79 | 80 | -- convert hex string ("414243") to byte string ("ABC") 81 | hextobytes = function(hexstr) 82 | if hexstr == nil then 83 | return nil 84 | end 85 | 86 | if #hexstr % 2 ~= 0 then 87 | error("illegal argument #1") 88 | end 89 | 90 | local bytestr = "" 91 | for i = 1, #hexstr, 2 do 92 | local s = hexstr:sub(i, i + 1) 93 | local n = tonumber(s, 16) 94 | if n == nil then 95 | error("illegal argument #1") 96 | end 97 | bytestr = bytestr .. string.char(n) 98 | end 99 | return bytestr 100 | end; 101 | 102 | -- convert byte string ("ABC") to hex string ("414243") 103 | bytestohex = function(bytestr) 104 | if bytestr == nil then 105 | return nil 106 | end 107 | 108 | local hexstr = "" 109 | for i = 1, #bytestr do 110 | hexstr = hexstr .. string.format("%02x", bytestr:byte(i)) 111 | end 112 | return hexstr 113 | end; 114 | 115 | -- reset current logging state 116 | clear = function(self) 117 | self.lastValue = {} 118 | self.waveformList = {} 119 | self.scoreChannel = {} 120 | self.scoreGlobal = {} 121 | self.tick = 0 122 | end; 123 | 124 | -- add current sound state to scoreChannel, this function is called from write() 125 | -- @param number target channel index 126 | -- @param VGMSoundChannel current sound state of a channel 127 | addChannelState = function(self, chIndex, curr) 128 | if self.scoreChannel[chIndex] == nil then 129 | self.scoreChannel[chIndex] = {} 130 | end 131 | if self.lastValue[chIndex] == nil then 132 | self.lastValue[chIndex] = {} 133 | end 134 | 135 | local score = self.scoreChannel[chIndex] 136 | local prev = self.lastValue[chIndex] 137 | local channelNumber = chIndex - 1 138 | 139 | local currVolume = curr.volume 140 | if currVolume < self.VOLUME_MUTE_THRESHOLD then 141 | currVolume = 0.0 142 | end 143 | if currVolume ~= prev.volume then 144 | table.insert(score, { 'volume_change', self.tick, channelNumber, currVolume }) 145 | prev.volume = currVolume 146 | end 147 | 148 | if currVolume ~= 0.0 then 149 | if curr.patch ~= prev.patch then 150 | local patchNumber = nil 151 | if type(curr.patch) == "number" then 152 | patchNumber = curr.patch 153 | else 154 | -- search in waveform table 155 | if self.waveformList[curr.type] ~= nil then 156 | for waveformIndex, waveform in ipairs(self.waveformList[curr.type]) do 157 | if curr.patch == waveform then 158 | patchNumber = waveformIndex - 1 159 | break 160 | end 161 | end 162 | else 163 | self.waveformList[curr.type] = {} 164 | end 165 | -- add new patch if needed 166 | if patchNumber == nil then 167 | patchNumber = #self.waveformList[curr.type] 168 | self.waveformList[curr.type][patchNumber + 1] = curr.patch 169 | end 170 | end 171 | table.insert(score, { 'patch_change', self.tick, channelNumber, patchNumber, curr.type }) 172 | prev.patch = curr.patch 173 | end 174 | 175 | if curr.panpot ~= prev.panpot then 176 | table.insert(score, { 'panpot_change', self.tick, channelNumber, curr.panpot }) 177 | prev.panpot = curr.panpot 178 | end 179 | if curr.midikey ~= prev.midikey then 180 | table.insert(score, { 'absolute_pitch_change', self.tick, channelNumber, curr.midikey }) 181 | prev.midikey = curr.midikey 182 | end 183 | end 184 | end; 185 | 186 | -- add current sound state to scoreGlobal, this function is called from write() 187 | addGlobalState = function(self) 188 | -- do nothing 189 | local score = self.scoreGlobal 190 | end; 191 | 192 | -- write current sound state to score, this function must be called every tick (frame) 193 | -- @param VGMSoundChannel[] current sound state of each channels 194 | write = function(self, channels) 195 | for chIndex, channel in ipairs(channels) do 196 | self:addChannelState(chIndex, channel) 197 | end 198 | self:addGlobalState() 199 | self.tick = self.tick + 1 200 | end; 201 | 202 | -- get FlMML patch command 203 | -- @param string patch type (wavememory, dpcm, etc.) 204 | -- @param number patch number 205 | -- @return string patch mml text 206 | getFlMMLPatchCmd = function(self, patchType, patchNumber) 207 | return string.format("/*%s:@%d*/", patchType, patchNumber) 208 | end; 209 | 210 | -- get FlMML waveform definition MML 211 | -- @return string waveform define mml 212 | getFlMMLWaveformDef = function(self) 213 | local mml = "" 214 | for waveChannelType, waveList in pairs(self.waveformList) do 215 | for waveIndex, waveValue in ipairs(waveList) do 216 | mml = mml .. string.format("/* %s-%d=%s */\n", waveChannelType, waveIndex - 1, waveValue) 217 | end 218 | end 219 | return mml 220 | end; 221 | 222 | -- get FlMML tuning for each patches 223 | -- @param number origNoteNumber input note number 224 | -- @param string patchType patch type (square, noise, etc.) 225 | -- @return number output note number 226 | getFlMMLNoteNumber = function(self, origNoteNumber, patchType) 227 | return origNoteNumber 228 | end; 229 | 230 | -- get FlMML text 231 | -- @return string FlMML text 232 | getFlMML = function(self) 233 | local MML_TICK_MUL = 1 234 | local getOctaveAndScale = function(midikey) 235 | if midikey == nil then 236 | return nil, nil 237 | end 238 | 239 | local oct = math.floor(midikey / 12) 240 | local scale = midikey % 12 241 | return oct, scale 242 | end 243 | local keyToMML = function(midikey) 244 | local oct, scale = getOctaveAndScale(midikey) 245 | local notetable = { "c", "c+", "d", "d+", "e", "f", "f+", "g", "g+", "a", "a+", "b" } 246 | return string.format("o%d", oct), notetable[1 + scale] 247 | end 248 | local tickToMML = function(tick) 249 | return string.format("%%%d", tick * MML_TICK_MUL) 250 | end 251 | local needsTie = function(flmmlPatchCmd) 252 | return flmmlPatchCmd ~= "@7" and flmmlPatchCmd ~= "@8" and flmmlPatchCmd:sub(1,2) ~= "@9" and flmmlPatchCmd ~= "@11" and flmmlPatchCmd ~= "@12" 253 | end 254 | 255 | local scores = self:getFlMMLScore() 256 | local mml = "" 257 | local flmmlPatchCmd = "" 258 | for scoreIndex, score in ipairs(scores) do 259 | local mmlArray = {} 260 | local noteInfo = nil 261 | local prev = { tick = 0, slurEventIndex = nil } 262 | for eventIndex, event in ipairs(score) do 263 | local eventName = event[1] 264 | local tick = event[2] 265 | local tickDiff = tick - prev.tick 266 | 267 | -- delta time 268 | assert(tickDiff >= 0) 269 | if tickDiff ~= 0 then 270 | local tickMML = tickToMML(tickDiff) 271 | if noteInfo then 272 | table.insert(mmlArray, string.format("%s%s", noteInfo.noteMML, tickMML)) 273 | -- NES/GB noise and DPCM has a problem with tie (&) 274 | -- for instance, c4&c4 doesn't work well. 275 | -- c4&4 works well, but it is problematic when 276 | -- you want to use something like c4&@v10c4 . 277 | -- Real NES RP2A03 doesn't need tie for them, though. 278 | -- Therefore, omit ties for these channels for now. 279 | if needsTie(flmmlPatchCmd) then 280 | table.insert(mmlArray, "&") 281 | prev.slurEventIndex = #mmlArray 282 | end 283 | else 284 | -- eliminate slur 285 | if prev.slurEventIndex then 286 | mmlArray[prev.slurEventIndex] = "" 287 | prev.slurEventIndex = nil 288 | end 289 | table.insert(mmlArray, string.format("r%s", tickMML)) 290 | end 291 | 292 | -- prev.tickChangeEventIndex = #mmlArray + 1 293 | -- prev.tickHasNote = false 294 | end 295 | prev.tick = tick 296 | 297 | if eventName == 'end_track' then 298 | -- do nothing 299 | elseif eventName == 'set_tempo' then 300 | -- 1 tick := (1/framerate)[sec] 301 | local framerate = 60000000.0 / event[3] 302 | local mmlBPM = MML_TICK_MUL * 60 * framerate / self.FLMML_TPQN 303 | table.insert(mmlArray, string.format("T%.2f", mmlBPM)) 304 | elseif eventName == 'volume_change' then 305 | table.insert(mmlArray, string.format("@X%d", event[4])) 306 | elseif eventName == 'panpot_change' then 307 | table.insert(mmlArray, string.format("@P%d", event[4])) 308 | elseif eventName == 'pitch_wheel_change' then 309 | table.insert(mmlArray, string.format("@D%d", event[4])) 310 | elseif eventName == 'patch_change' then 311 | flmmlPatchCmd = self:getFlMMLPatchCmd(event[5], event[4]) 312 | table.insert(mmlArray, flmmlPatchCmd) 313 | elseif eventName == 'note_on' and event[5] ~= 0 then 314 | -- assert: next event has different tick, no events left at this time 315 | if eventIndex >= #score or tick >= score[eventIndex + 1][2] then 316 | error("FlMML conversion: unsupported event order, note on must be last.") 317 | end 318 | local nextTick = score[eventIndex + 1][2] 319 | 320 | -- register note info 321 | local oct, scale = getOctaveAndScale(event[4]) 322 | local octMML, noteMML = keyToMML(event[4]) 323 | noteInfo = { tick = event[2], midikey = event[4], velocity = event[5], octMML = octMML, noteMML = noteMML } 324 | -- omit octave command 325 | if oct == prev.oct then 326 | octMML = "" 327 | end 328 | 329 | -- write note command 330 | local noteTickDiff = nextTick - tick 331 | table.insert(mmlArray, string.format("%s%s%s", octMML, noteMML, tickToMML(noteTickDiff))) 332 | if needsTie(flmmlPatchCmd) then 333 | table.insert(mmlArray, "&") 334 | prev.slurEventIndex = #mmlArray 335 | end 336 | prev.tick = nextTick -- cancel next tick diff event 337 | 338 | -- local octMML, noteMML = keyToMML(event[4]) 339 | -- noteInfo = { index = #mmlArray + 1, tick = event[2], tickTie = event[2], midikey = event[4], velocity = event[5], octMML = octMML, noteMML = noteMML } 340 | -- prev.tickHasNote = true 341 | elseif eventName == 'note_off' or (eventName == 'note_on' and event[5] == 0) then 342 | -- table.insert(mmlArray, noteInfo.index, noteInfo.octMML .. noteInfo.noteMML .. tickToMML(tick - noteInfo.tickTie)) 343 | noteInfo = nil 344 | else 345 | --table.insert(mmlArray, "\n/* " .. event[1] .. " */\n") 346 | print(string.format("FlMML conversion: unsupported event '%s'", event[1])) 347 | end 348 | end 349 | table.insert(mmlArray, ";\n") 350 | 351 | -- close note tie 352 | if prev.slurEventIndex then 353 | mmlArray[prev.slurEventIndex] = "" 354 | prev.slurEventIndex = nil 355 | end 356 | 357 | -- mml join 358 | mml = mml .. "@E1,0,0,180,0Q16" 359 | for mmlPartIndex, mmlPart in ipairs(mmlArray) do 360 | mml = mml .. mmlPart 361 | end 362 | end 363 | mml = mml .. self:getFlMMLWaveformDef() 364 | return mml 365 | end; 366 | 367 | -- get score for FlMML conversion 368 | -- @return string score array 369 | getFlMMLScore = function(self) 370 | local mmlscore = {} 371 | 372 | -- global events 373 | if self.scoreGlobal then 374 | table.insert(mmlscore, self:scoreAddEndOfTrack(self.scoreGlobal)) 375 | end 376 | 377 | -- channel events 378 | if self.scoreChannel then 379 | for chIndex, score in ipairs(self.scoreChannel) do 380 | local channelNumber = chIndex - 1 381 | local mscore = self:scoreRemoveDuplicatedEvent(self:scoreConvertToFlMML(self:scoreBuildNote(self:scoreAddEndOfTrack(score)))) 382 | table.insert(mmlscore, mscore) 383 | end 384 | end 385 | 386 | return mmlscore 387 | end; 388 | 389 | -- get MIDI TPQN (integer) 390 | getTPQN = function(self) 391 | return 60 392 | end; 393 | 394 | -- get MIDI score 395 | -- @return string score array for MIDI.lua 396 | getMidiScore = function(self) 397 | local midiscore = {} 398 | 399 | -- global events 400 | if self.scoreGlobal then 401 | table.insert(midiscore, self:scoreAddEndOfTrack(self.scoreGlobal)) 402 | end 403 | 404 | -- channel events 405 | if self.scoreChannel then 406 | for chIndex, score in ipairs(self.scoreChannel) do 407 | local channelNumber = chIndex - 1 408 | local mscore = self:scoreRemoveDuplicatedEvent(self:scoreConvertToMidi(self:scoreBuildNote(self:scoreAddEndOfTrack(score)))) 409 | table.insert(mscore, 1, { 'control_change', 0, channelNumber, 101, 0 }) 410 | table.insert(mscore, 2, { 'control_change', 0, channelNumber, 100, 0 }) 411 | table.insert(mscore, 3, { 'control_change', 0, channelNumber, 6, self.MIDI_PITCHBEND_RANGE }) 412 | table.insert(midiscore, mscore) 413 | end 414 | end 415 | 416 | return { self:getTPQN(), unpack(midiscore) } 417 | end; 418 | 419 | -- get readable text format of score (debug function) 420 | -- @param score target to convert 421 | getScoreText = function(self, score) 422 | local str = "" 423 | for i, event in ipairs(score) do 424 | for j, v in ipairs(event) do 425 | if j > 1 then 426 | str = str .. "\t" 427 | end 428 | str = str .. tostring(v) 429 | end 430 | str = str .. "\n" 431 | end 432 | return str 433 | end; 434 | 435 | -- write FlMML text to file 436 | -- @param string filename output filename 437 | writeFlMMLFile = function(self, filename) 438 | local file = assert(io.open(filename, "w")) 439 | file:write(self:getFlMML(self)) 440 | file:close() 441 | end; 442 | 443 | -- write MIDI data to file 444 | -- @param string filename output filename 445 | writeMidiFile = function(self, filename) 446 | local MIDI = require("MIDI") 447 | local file = assert(io.open(filename, 'wb')) 448 | file:write(MIDI.score2midi(self:getMidiScore())) 449 | file:close() 450 | end; 451 | 452 | -- write readable text to file (debug function) 453 | -- @param string filename output filename 454 | writeTextFile = function(self, filename) 455 | local file = assert(io.open(filename, "w")) 456 | 457 | -- channel events 458 | for chIndex, score in ipairs(self.scoreChannel) do 459 | file:write(string.format("/* Channel %d */\n", chIndex - 1)) 460 | 461 | local modscore = self:scoreRemoveDuplicatedEvent(self:scoreBuildNote(self:scoreAddEndOfTrack(score))) 462 | file:write(self:getScoreText(modscore)) 463 | file:write("\n") 464 | end 465 | 466 | -- global events 467 | file:write("/* Global */\n") 468 | file:write(self:getScoreText(self.scoreGlobal)) 469 | file:write("\n") 470 | 471 | -- wavetable table 472 | for waveChannelType, waveList in pairs(self.waveformList) do 473 | file:write(string.format("/* Waveform (%s) */\n", waveChannelType)) 474 | for waveIndex, waveValue in ipairs(waveList) do 475 | file:write(waveIndex .. "\t" .. waveValue .. "\n") 476 | end 477 | file:write("\n") 478 | end 479 | 480 | file:close() 481 | end; 482 | 483 | -- event conversion: convert patch event for MIDI 484 | -- @param source 'patch_change' event 485 | eventPatchToMidi = function(self, event) 486 | return { { 'patch_change', event[2], event[3], event[4] } } 487 | end; 488 | 489 | -- score manipulation: add end of track 490 | -- @param score manipulation target 491 | scoreAddEndOfTrack = function(self, scoreIn) 492 | local score = {} 493 | for i, eventIn in ipairs(scoreIn) do 494 | local event = {} 495 | for j, v in ipairs(eventIn) do 496 | event[j] = v 497 | end 498 | table.insert(score, event) 499 | end 500 | table.insert(score, { 'end_track', self.tick }) 501 | return score 502 | end; 503 | 504 | -- score manipulation: remove duplicated events 505 | -- @param score manipulation target 506 | scoreRemoveDuplicatedEvent = function(self, scoreIn) 507 | local score = {} 508 | local prev = {} 509 | for i, eventIn in ipairs(scoreIn) do 510 | if eventIn[1] == 'control_change' or eventIn[1] == 'volume_change' or eventIn[1] == 'panpot_change' or eventIn[1] == 'pitch_wheel_change' or eventIn[1] == 'absolute_pitch_change' then 511 | local name = eventIn[1] 512 | local value = eventIn[4] 513 | if name == 'control_change' then 514 | name = name .. string.format("-%d", eventIn[4]) 515 | value = eventIn[5] 516 | end 517 | 518 | if value == prev[name] then 519 | eventIn = nil 520 | else 521 | prev[name] = value 522 | end 523 | end 524 | 525 | if eventIn ~= nil then 526 | local event = {} 527 | for j, v in ipairs(eventIn) do 528 | event[j] = v 529 | end 530 | table.insert(score, event) 531 | end 532 | end 533 | return score 534 | end; 535 | 536 | -- score manipulation: build note from volume and pitch 537 | -- @param score manipulation target 538 | scoreBuildNote = function(self, scoreIn) 539 | -- find event by name 540 | -- @return number event index, nil if not found 541 | local findEventByName = function(events, eventName) 542 | for i = #events, 1, -1 do 543 | local eventIn = events[i] 544 | if eventIn[1] == eventName then 545 | return i 546 | end 547 | end 548 | return nil 549 | end 550 | -- add note off event 551 | local addNoteOffEvent = function(events, tick, channelNumber, noteNumber, lastEvent) 552 | local indexLast = #events + 1 553 | if findEventByName(events, 'end_track') then 554 | indexLast = #events 555 | end 556 | table.insert(events, lastEvent and indexLast or 1, { 'note_off', tick, channelNumber, noteNumber, 0 }) 557 | end 558 | -- add note on event 559 | local addNoteOnEvent = function(events, tick, channelNumber, noteNumber, velocity) 560 | local indexLast = #events + 1 561 | if findEventByName(events, 'end_track') then 562 | indexLast = #events 563 | end 564 | table.insert(events, indexLast, { 'note_on', tick, channelNumber, noteNumber, velocity }) 565 | end 566 | -- remove specified event 567 | local removeEvent = function(events, eventName) 568 | for i = #events, 1, -1 do 569 | local event = events[i] 570 | if event[1] == eventName then 571 | table.remove(events, i) 572 | end 573 | end 574 | end 575 | -- replace specified event value 576 | local replaceEventValue = function(events, eventName, value) 577 | local eventIndex = findEventByName(events, eventName) 578 | if eventIndex then 579 | local eventIn = events[eventIndex] 580 | local event = {} 581 | for j, v in ipairs(eventIn) do 582 | event[j] = v 583 | end 584 | event[4] = value 585 | events[eventIndex] = event 586 | end 587 | end 588 | -- add absolute pitch event to note, if there is not 589 | local addPitchToNoteIfNeeded = function(events, absPitchEvent) 590 | if absPitchEvent == nil then 591 | return 592 | end 593 | assert(absPitchEvent[1] == 'absolute_pitch_change') 594 | 595 | -- if pitch event already exists, do not add more 596 | local eventIndex = findEventByName(events, 'absolute_pitch_change') 597 | if eventIndex then 598 | return 599 | end 600 | 601 | -- position needs to be before note on 602 | eventIndex = findEventByName(events, 'note_on') 603 | if eventIndex == nil then 604 | eventIndex = #events + 1 605 | end 606 | 607 | -- insert event 608 | local event = {} 609 | for j, v in ipairs(absPitchEvent) do 610 | event[j] = v 611 | end 612 | table.insert(events, eventIndex, event) 613 | end 614 | -- convert pitch bend absolute to relative 615 | local pitchAbsToRel = function(events, noteNumber) 616 | for i = #events, 1, -1 do 617 | local event = events[i] 618 | if event[1] == 'absolute_pitch_change' then 619 | if noteNumber ~= nil then 620 | local relPitch = event[4] - noteNumber 621 | events[i] = { 'pitch_wheel_change', event[2], event[3], relPitch } 622 | else 623 | table.remove(events, i) 624 | end 625 | end 626 | end 627 | end 628 | 629 | local score = {} 630 | local eventIndex = 1 631 | local prev = { tick = 0 } 632 | local channelNumber = nil 633 | local lastAbsPitchEvent = nil 634 | while eventIndex <= #scoreIn do 635 | local curr = {} 636 | local new_ = {} 637 | 638 | -- collect events at the same timing 639 | local events = { scoreIn[eventIndex] } 640 | curr.tick = scoreIn[eventIndex][2] 641 | while (eventIndex + 1) <= #scoreIn and scoreIn[eventIndex + 1][2] == curr.tick do 642 | eventIndex = eventIndex + 1 643 | table.insert(events, scoreIn[eventIndex]) 644 | end 645 | 646 | -- get new volume/pitch, remove duplicated events (reverse order) 647 | -- channelNumber = nil 648 | for i = #events, 1, -1 do 649 | local event = events[i] 650 | if event[1] == 'volume_change' or event[1] == 'absolute_pitch_change' then 651 | if channelNumber == nil then 652 | channelNumber = event[3] 653 | else 654 | assert(channelNumber == event[3]) 655 | end 656 | 657 | if event[1] == 'volume_change' then 658 | if new_.volume == nil then 659 | new_.volume = event[4] 660 | else 661 | table.remove(events, i) 662 | end 663 | elseif event[1] == 'absolute_pitch_change' then 664 | if new_.midikey == nil then 665 | new_.midikey = event[4] 666 | lastAbsPitchEvent = { unpack(event) } 667 | else 668 | table.remove(events, i) 669 | end 670 | end 671 | elseif event[1] == 'pitch_wheel_change' then 672 | error("relative pitch event is not supported.") 673 | end 674 | end 675 | -- set current volume/pitch value 676 | curr.volume = new_.volume or prev.volume 677 | curr.midikey = new_.midikey or prev.midikey 678 | curr.noteNumber = prev.noteNumber 679 | 680 | -- note on/off detection main 681 | local requireNoteOff = false 682 | local requireNoteOn = false 683 | if curr.volume and curr.volume ~= 0 then 684 | if prev.volume and prev.volume ~= 0 then 685 | local pitchDiff = math.abs(curr.midikey - prev.midikey) 686 | local volumeDistance = curr.volume - prev.volume 687 | if pitchDiff > 0 then 688 | if pitchDiff >= self.NOTE_PITCH_THRESHOLD then 689 | local nextNoteNumber = self.round(curr.midikey) 690 | if nextNoteNumber ~= prev.noteNumber then 691 | -- new note! (frequency changed) 692 | requireNoteOff = true 693 | requireNoteOn = true 694 | end 695 | end 696 | end 697 | if volumeDistance > 0 and volumeDistance >= self.NOTE_VOLUME_THRESHOLD then 698 | -- new note! (volume up) 699 | requireNoteOff = true 700 | requireNoteOn = true 701 | end 702 | prev.midikey = curr.midikey 703 | else 704 | -- new note! (from volume 0) 705 | requireNoteOn = true 706 | end 707 | else 708 | if prev.volume and prev.volume ~= 0 then 709 | -- end of note 710 | requireNoteOff = true 711 | removeEvent(events, 'volume_change') 712 | else 713 | -- no sound / rest before the first note 714 | removeEvent(events, 'volume_change') 715 | end 716 | end 717 | if requireNoteOff then 718 | addNoteOffEvent(events, curr.tick, channelNumber, prev.noteNumber) 719 | if not requireNoteOn then 720 | prev.noteNumber = nil 721 | end 722 | --removeEvent(events, 'volume_change') 723 | end 724 | if requireNoteOn then 725 | curr.noteNumber = self.round(curr.midikey) 726 | addNoteOnEvent(events, curr.tick, channelNumber, curr.noteNumber, self.NOTE_VELOCITY) 727 | prev.noteNumber = curr.noteNumber 728 | 729 | -- add possibly missing relative pitch event 730 | -- we need to duplicate pitch event when the situation is like the following: 731 | -- note on -> raise pitch quite slowly (note continues) -> volume down -> volume up ("new note" detected here) 732 | -- at the "new note" timing, there is no pitch event, because pitch doesn't changed from the previous tick. 733 | -- however, we need a new one because we will use "relative" pitch change event, 734 | -- and the "base key" for the relative pitch gets changed at the new note, even though the frequency doesn't change. 735 | -- anyway, we need a dirty fix here. 736 | lastAbsPitchEvent[2] = curr.tick 737 | addPitchToNoteIfNeeded(events, lastAbsPitchEvent) 738 | 739 | -- pitch bend remove hack 740 | if math.abs(curr.midikey - curr.noteNumber) < self.NOTE_PITCH_STRIP_THRESHOLD then 741 | -- set pitch=0, duplication remover will clean up them :) 742 | replaceEventValue(events, 'absolute_pitch_change', curr.noteNumber) 743 | lastAbsPitchEvent[4] = curr.noteNumber 744 | end 745 | end 746 | 747 | -- update status 748 | prev.midikey = curr.midikey 749 | prev.volume = curr.volume 750 | 751 | -- convert pitch bend absolute to relative 752 | pitchAbsToRel(events, curr.noteNumber) 753 | 754 | -- finally... 755 | if eventIndex == #scoreIn then 756 | -- add missing note off 757 | if prev.noteNumber then 758 | addNoteOffEvent(events, curr.tick, channelNumber, prev.noteNumber, true) 759 | prev.noteNumber = nil 760 | end 761 | end 762 | 763 | -- copy the modified events to output score 764 | for i, eventIn in ipairs(events) do 765 | local event = {} 766 | for j, v in ipairs(eventIn) do 767 | event[j] = v 768 | end 769 | table.insert(score, event) 770 | end 771 | 772 | eventIndex = eventIndex + 1 773 | end 774 | return score 775 | end; 776 | 777 | -- score manipulation: convert to FlMML compatible score 778 | -- @param score manipulation target 779 | scoreConvertToFlMML = function(self, scoreIn) 780 | local score = {} 781 | local patchType = nil 782 | for i, event in ipairs(scoreIn) do 783 | if event[1] == 'volume_change' then 784 | local value = event[4] 785 | assert(value >= 0.0 and value <= 1.0) 786 | 787 | event = { 'volume_change', event[2], event[3], self.round(value * 127) } 788 | table.insert(score, event) 789 | elseif event[1] == 'panpot_change' then 790 | local value = event[4] 791 | assert(value >= 0.0 and value <= 1.0) 792 | 793 | event = { 'panpot_change', event[2], event[3], self.round(value * 126) + 1 } 794 | table.insert(score, event) 795 | elseif event[1] == 'pitch_wheel_change' then 796 | local value = event[4] 797 | event = { 'pitch_wheel_change', event[2], event[3], self.round(100 * value) } 798 | table.insert(score, event) 799 | elseif event[1] == 'patch_change' then 800 | patchType = event[5] 801 | table.insert(score, event) 802 | elseif event[1] == 'note_on' then 803 | event = { 'note_on', event[2], event[3], self:getFlMMLNoteNumber(event[4], patchType), event[5] } 804 | table.insert(score, event) 805 | elseif event[1] == 'note_off' then 806 | event = { 'note_off', event[2], event[3], self:getFlMMLNoteNumber(event[4], patchType), event[5] } 807 | table.insert(score, event) 808 | elseif event[1] == 'absolute_pitch_change' then 809 | error("'absolute_pitch_change' need to be converted before scoreConvertToFlMML.") 810 | else 811 | table.insert(score, event) 812 | end 813 | end 814 | return score 815 | end; 816 | 817 | -- score manipulation: convert to MIDI compatible event 818 | -- @param score manipulation target 819 | scoreConvertToMidi = function(self, scoreIn) 820 | local score = {} 821 | for i, event in ipairs(scoreIn) do 822 | if event[1] == 'patch_change' then 823 | local patchEvents = self:eventPatchToMidi(event) 824 | for j, patchEvent in ipairs(patchEvents) do 825 | table.insert(score, patchEvent) 826 | end 827 | elseif event[1] == 'volume_change' then 828 | local value = event[4] 829 | assert(value >= 0.0 and value <= 1.0) 830 | 831 | if not self.MIDI_LINEAR_CONVERSION then 832 | -- gain[dB] = 40 * log10(cc7/127) 833 | value = math.sqrt(value) 834 | end 835 | 836 | event = { 'control_change', event[2], event[3], 7, self.round(value * 127) } 837 | table.insert(score, event) 838 | elseif event[1] == 'panpot_change' then 839 | local value = event[4] 840 | assert(value >= 0.0 and value <= 1.0) 841 | 842 | -- TODO: decent panpot curve 843 | if not self.MIDI_LINEAR_CONVERSION then 844 | -- GM2 recommended formula: 845 | -- Left Channel Gain [dB] = 20*log(cos(PI/2*max(0,cc#10-1)/126)) 846 | -- Right Channel Gain [dB] = 20*log(sin(PI/2*max(0,cc#10-1)/126)) 847 | end 848 | 849 | event = { 'control_change', event[2], event[3], 10, self.round(value * 126) + 1 } 850 | table.insert(score, event) 851 | elseif event[1] == 'pitch_wheel_change' then 852 | local value = event[4] 853 | 854 | if value < -self.MIDI_PITCHBEND_RANGE or value > self.MIDI_PITCHBEND_RANGE then 855 | print(string.format("Warning: pitch bend range overflow <%f> cent at tick <%d> channel <%d>.", value * 100, event[2], event[3])) 856 | value = math.min(self.MIDI_PITCHBEND_RANGE, math.max(-self.MIDI_PITCHBEND_RANGE, value)) 857 | end 858 | 859 | event = { 'pitch_wheel_change', event[2], event[3], math.min(self.round(value / self.MIDI_PITCHBEND_RANGE * 8192), 8191) } 860 | table.insert(score, event) 861 | elseif event[1] == 'note_off' then 862 | event = { 'note_on', event[2], event[3], event[4], 0 } 863 | table.insert(score, event) 864 | elseif event[1] == 'absolute_pitch_change' then 865 | error("'absolute_pitch_change' need to be converted before scoreConvertToMidi.") 866 | else 867 | table.insert(score, event) 868 | end 869 | end 870 | return score 871 | end; 872 | 873 | -- static gbNoiseFreqRegToNote 874 | -- @param number noise frequency (Hz) 875 | -- @return number FlMML compatible noise note number 876 | gbNoiseFreqRegToNote = function(freq) 877 | local flmmlGbNoiseLookup = { 878 | 0x000002, 0x000004, 0x000008, 0x00000c, 0x000010, 0x000014, 0x000018, 0x00001c, 879 | 0x000020, 0x000028, 0x000030, 0x000038, 0x000040, 0x000050, 0x000060, 0x000070, 880 | 0x000080, 0x0000a0, 0x0000c0, 0x0000e0, 0x000100, 0x000140, 0x000180, 0x0001c0, 881 | 0x000200, 0x000280, 0x000300, 0x000380, 0x000400, 0x000500, 0x000600, 0x000700, 882 | 0x000800, 0x000a00, 0x000c00, 0x000e00, 0x001000, 0x001400, 0x001800, 0x001c00, 883 | 0x002000, 0x002800, 0x003000, 0x003800, 0x004000, 0x005000, 0x006000, 0x007000, 884 | 0x008000, 0x00a000, 0x00c000, 0x00e000, 0x010000, 0x014000, 0x018000, 0x01c000, 885 | 0x020000, 0x028000, 0x030000, 0x038000, 0x040000, 0x050000, 0x060000, 0x070000 886 | } 887 | 888 | -- search in table 889 | for index, targetFreq in ipairs(flmmlGbNoiseLookup) do 890 | if freq == targetFreq then 891 | return index - 1 892 | end 893 | end 894 | 895 | error(string.format("illegal gameboy noise frequency value 0x%06x", freq)) 896 | end; 897 | 898 | -- static nesNoiseFreqToNote 899 | -- @param number noise frequency (Hz) 900 | -- @return number FlMML compatible noise note number 901 | nesNoiseFreqToNote = function(freq) 902 | local flmmlNESNoiseLookup = { 903 | 0x002, 0x004, 0x008, 0x010, 0x020, 0x030, 0x040, 0x050, 904 | 0x065, 0x07f, 0x0be, 0x0fe, 0x17d, 0x1fc, 0x3f9, 0x7f2 905 | } 906 | 907 | -- search in table (search the nearest one) 908 | local bestDiff = math.huge 909 | local bestIndex = 1 910 | for index, targetFreqReg in ipairs(flmmlNESNoiseLookup) do 911 | local targetFreq = 1789772.5 / targetFreqReg 912 | local diff = math.abs(freq - targetFreq) 913 | if diff < bestDiff then 914 | bestIndex = index 915 | bestDiff = diff 916 | else 917 | break 918 | end 919 | end 920 | 921 | return bestIndex - 1 922 | end; 923 | 924 | -- static gbNoiseFreqToNote 925 | -- @param number noise frequency register value 926 | -- @return number FlMML compatible noise note number 927 | gbNoiseFreqToNote = function(freq) 928 | local flmmlGbNoiseLookup = { 929 | 0x000002, 0x000004, 0x000008, 0x00000c, 0x000010, 0x000014, 0x000018, 0x00001c, 930 | 0x000020, 0x000028, 0x000030, 0x000038, 0x000040, 0x000050, 0x000060, 0x000070, 931 | 0x000080, 0x0000a0, 0x0000c0, 0x0000e0, 0x000100, 0x000140, 0x000180, 0x0001c0, 932 | 0x000200, 0x000280, 0x000300, 0x000380, 0x000400, 0x000500, 0x000600, 0x000700, 933 | 0x000800, 0x000a00, 0x000c00, 0x000e00, 0x001000, 0x001400, 0x001800, 0x001c00, 934 | 0x002000, 0x002800, 0x003000, 0x003800, 0x004000, 0x005000, 0x006000, 0x007000, 935 | 0x008000, 0x00a000, 0x00c000, 0x00e000, 0x010000, 0x014000, 0x018000, 0x01c000, 936 | 0x020000, 0x028000, 0x030000, 0x038000, 0x040000, 0x050000, 0x060000, 0x070000 937 | } 938 | 939 | -- search in table (search the nearest one) 940 | local bestDiff = math.huge 941 | local bestIndex = 1 942 | for index, targetFreqReg in ipairs(flmmlGbNoiseLookup) do 943 | local targetFreq = 1048576.0 / targetFreqReg 944 | local diff = math.abs(freq - targetFreq) 945 | if diff < bestDiff then 946 | bestIndex = index 947 | bestDiff = diff 948 | else 949 | break 950 | end 951 | end 952 | 953 | return bestIndex - 1 954 | end; 955 | } 956 | 957 | self:clear() 958 | return self 959 | end 960 | --------------------------------------------------------------------------------