├── README.md ├── argparse.lua ├── midi2note.lua ├── midi2pico.lua └── musichax-stub.lua /README.md: -------------------------------------------------------------------------------- 1 | # midi2pico 2 | A Midi to PICO-8 converter. 3 | 4 | Requires [MIDI.lua](http://www.pjb.com.au/comp/lua/MIDI.html) [(direct link)](http://www.pjb.com.au/comp/lua/MIDI.lua) 5 | ``` 6 | # luarocks install midi 7 | ``` 8 | 9 | ## Usage: 10 | ``` 11 | lua midi2pico.lua somesong.mid songdata.p8 12 | ``` 13 | Various options are available, run the program with no arguments to get help. 14 | 15 | ## Tips: 16 | * Mute problematic channels with `--mute`, timidity's `--mute` argument can aid in finding problematic channels. 17 | * Halve the time division to possibly allow more mid-note effects. 18 | * Drums are problematic. Use `--dshift` to shift drum pitch, `--drumvol=n` to change drum volume, or `--mute=10` to remove drums all together. 19 | -------------------------------------------------------------------------------- /argparse.lua: -------------------------------------------------------------------------------- 1 | -- Simple argument parser borrowed from OpenComputers 2 | return function(...) 3 | local params = table.pack(...) 4 | local args = {} 5 | local options = {} 6 | local doneWithOptions = false 7 | for i = 1, params.n do 8 | local param = params[i] 9 | if not doneWithOptions and type(param) == "string" then 10 | if param == "--" then 11 | doneWithOptions = true 12 | elseif param:sub(1, 2) == "--" then 13 | if param:match("%-%-(.-)=") ~= nil then 14 | options[param:match("%-%-(.-)=")] = param:match("=(.*)") 15 | else 16 | options[param:sub(3)] = true 17 | end 18 | elseif param:sub(1, 1) == "-" and param ~= "-" then 19 | for j = 2, #param do 20 | options[param:sub(j, j)] = true 21 | end 22 | else 23 | table.insert(args, param) 24 | end 25 | else 26 | table.insert(args, param) 27 | end 28 | end 29 | return args, options 30 | end 31 | -------------------------------------------------------------------------------- /midi2note.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | local parse=require("argparse") 3 | 4 | local args, opts=parse(...) 5 | 6 | local function print(...) 7 | local args=table.pack(...) 8 | for i=1, args.n do 9 | args[i]=tostring(args[i]) 10 | end 11 | io.stderr:write(table.concat(args, "\t").."\n") 12 | end 13 | 14 | if #args < 1 then 15 | print("Usage: " .. (arg and arg[0] or "midi2note") .. " midifile [notefile]") 16 | return 0 17 | end 18 | 19 | local file, err=io.open(args[1], "rb") 20 | if not file then 21 | print(err) 22 | os.exit(1) 23 | end 24 | local data=file:read("*a") 25 | file:close() 26 | 27 | local text_events={ 28 | text_event=true, 29 | copyright_text_event=true, 30 | track_name=true, 31 | instrument_name=true, 32 | lyric=true, 33 | marker=true, 34 | cue_point=true, 35 | text_event_08=true, 36 | text_event_09=true, 37 | text_event_0a=true, 38 | text_event_0b=true, 39 | text_event_0c=true, 40 | text_event_0d=true, 41 | text_event_0e=true, 42 | text_event_0f=true, 43 | } 44 | local function score2note(score) 45 | local note={score[1]} 46 | local trackpos={} 47 | local nscore=#score 48 | for i=2, nscore do 49 | trackpos[i]=1 50 | end 51 | while true do 52 | local ltime, tpos=math.huge 53 | for i=2, nscore do 54 | local event=score[i][trackpos[i]] 55 | if event then 56 | local ttime=event[2] 57 | if ttime < ltime then 58 | ltime, tpos=ttime, i 59 | end 60 | end 61 | end 62 | if not tpos then break end 63 | local event=score[tpos][trackpos[tpos]] 64 | score[tpos][trackpos[tpos]]=nil 65 | trackpos[tpos]=trackpos[tpos] + 1 66 | table.insert(event, 3, tpos-2) 67 | -- Merge text events into one event type 68 | local kind=event[1] 69 | if text_events[kind] then 70 | kind=kind:gsub("_event", ""):gsub("_text", "") 71 | table.insert(event, 4, kind) 72 | event[1]="text" 73 | end 74 | note[#note+1]=event 75 | end 76 | return note 77 | end 78 | 79 | local midi=require("MIDI") 80 | 81 | local note=score2note(midi.midi2score(data)) 82 | 83 | local outfile, err 84 | if args[2] then 85 | print("Writing to '" .. args[2] .. "'") 86 | outfile, err=io.open(args[2], "wb") 87 | if not outfile then 88 | error(err, 0) 89 | end 90 | else 91 | print("Writing to stdout") 92 | outfile=io.stdout 93 | end 94 | outfile:write("Ticks per beat: " .. note[1].."\n") 95 | for i=2, #note do 96 | outfile:write(table.concat(note[i], ", ").."\n") 97 | end 98 | if args[2] then 99 | outfile:close() 100 | end 101 | -------------------------------------------------------------------------------- /midi2pico.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | 3 | local function print(...) 4 | local args=table.pack(...) 5 | for i=1, args.n do 6 | args[i]=tostring(args[i]) 7 | end 8 | io.stderr:write(table.concat(args, "\t").."\n") 9 | end 10 | 11 | -- LuaJIT and Lua5.1 compatibility 12 | if not table.pack then 13 | function table.pack(...) return {n=select("#", ...), ...} end 14 | end 15 | if not table.unpack then 16 | table.unpack = unpack 17 | end 18 | 19 | local bit=bit 20 | if not bit then 21 | local ok 22 | ok, bit=pcall(require, "bit32") 23 | if not ok then 24 | -- Fallback on pure lua bit implementation 25 | local xtab={ 26 | { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15}, 27 | { 1, 0, 3, 2, 5, 4, 7, 6, 9, 8,11,10,13,12,15,14}, 28 | { 2, 3, 0, 1, 6, 7, 4, 5,10,11, 8, 9,14,15,12,13}, 29 | { 3, 2, 1, 0, 7, 6, 5, 4,11,10, 9, 8,15,14,13,12}, 30 | { 4, 5, 6, 7, 0, 1, 2, 3,12,13,14,15, 8, 9,10,11}, 31 | { 5, 4, 7, 6, 1, 0, 3, 2,13,12,15,14, 9, 8,11,10}, 32 | { 6, 7, 4, 5, 2, 3, 0, 1,14,15,12,13,10,11, 8, 9}, 33 | { 7, 6, 5, 4, 3, 2, 1, 0,15,14,13,12,11,10, 9, 8}, 34 | { 8, 9,10,11,12,13,14,15, 0, 1, 2, 3, 4, 5, 6, 7}, 35 | { 9, 8,11,10,13,12,15,14, 1, 0, 3, 2, 5, 4, 7, 6}, 36 | {10,11, 8, 9,14,15,12,13, 2, 3, 0, 1, 6, 7, 4, 5}, 37 | {11,10, 9, 8,15,14,13,12, 3, 2, 1, 0, 7, 6, 5, 4}, 38 | {12,13,14,15, 8, 9,10,11, 4, 5, 6, 7, 0, 1, 2, 3}, 39 | {13,12,15,14, 9, 8,11,10, 5, 4, 7, 6, 1, 0, 3, 2}, 40 | {14,15,12,13,10,11, 8, 9, 6, 7, 4, 5, 2, 3, 0, 1}, 41 | {15,14,13,12,11,10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}, 42 | } 43 | local ff=2^32-1 44 | bit={} 45 | bit.bxor=function(a, b) 46 | local n, m=0, 1 47 | while a > 0 and b > 0 do 48 | local a2, b2=a%16, b%16 49 | n=n+xtab[a2+1][b2+1]*m 50 | a=(a-a2)/16 51 | b=(b-b2)/16 52 | m=m*16 53 | end 54 | n=n+a*m+b*m 55 | return n 56 | end 57 | bit.band=function(a, b) return ((a+b)-bit.bxor(a, b))/2 end 58 | bit.bor=function(a, b) return ff-bit.band(ff-a, ff-b) end 59 | bit.lshift=function(a, b) 60 | return a*2^b 61 | end 62 | bit.rshift=function(a, b) 63 | return math.floor(a/2^b) 64 | end 65 | end 66 | end 67 | 68 | local ok, midi=pcall(require, "MIDI") 69 | if not ok then 70 | print(midi) 71 | print("MIDI api is missing, please install via: luarocks install midi") 72 | os.exit(1) 73 | end 74 | 75 | local parse=require("argparse") 76 | 77 | local args, opts=parse(...) 78 | 79 | function math.round(n) 80 | return math.floor(n+0.5) 81 | end 82 | 83 | if #args < 1 or opts.help then 84 | print([[ 85 | Usage: ]] .. (arg and arg[0] or "midi2pico") .. [[ midi-file [p8-data] 86 | 87 | Options: 88 | --maxvol Maximum volume (0-7, 5) 89 | --drumvol Drum volume, (0-7, 2) 90 | --chvol Per channel volume (Comma separated, ch:vol format) 91 | --div Time division slices (Auto) 92 | --level Logging level 93 | --speed Music speed (0-255) 94 | --mute Mute channels (Comma separated, 0=All) 95 | --mutet Mute tracks (Comma separated) 96 | --mode Arragement mode (blob/channel/track) 97 | --shift Pitch shift, instruments (0) 98 | --dshift Pitch shift, drums (0) 99 | 100 | All options above take the form: --name=value 101 | 102 | --debug Output sfx and pattern info at end of lines 103 | --musichax Write a special program to stream additional audio from gfx 104 | --no2ndpass Skip second corrective pass 105 | --nopwheel Ignore pitch wheel data 106 | --novol Ignore volume data 107 | --noexpr Ignore expression data 108 | --notrunc Keep going despite no more sfx 109 | --stub Write a lua stub to automatically play the generated music 110 | 111 | --ignorediv Ignore bad time divisions 112 | --fixdivone Correct time divisions off by one 113 | --analysis Analyse and report on time information 114 | 115 | All options above take the form: --name 116 | 117 | MusicHAX: 118 | MusicHAX is a system to store more audio data than pico-8 normally allows, 119 | audio data is stored in gfx areas and copied into sfx as needed. Enabling 120 | this option will use this system and also output a lua stub to process and 121 | play the music. 122 | ]]) 123 | return 124 | end 125 | 126 | local function arg2num(name) 127 | val=tonumber(tostring(opts[name]), 10) 128 | if not val then 129 | error("Invalid value for option '" .. name .. "': " .. tostring(opts[name]), 0) 130 | end 131 | return val 132 | end 133 | 134 | -- Instrument to PICO-8 Map 135 | local picoinstr={ 136 | [0]={1, 0, 0, 5}, 137 | [1]={1, 0, 0, 5}, 138 | [3]={1, 2, 2, 5}, 139 | [4]={0, 0, 0, 5}, 140 | [5]={5, 0, 0, 5}, 141 | [7]={4, 0, 0, 5}, 142 | [11]={1, 0, 0, 5}, 143 | [16]={5, 0, 0, 5}, 144 | [17]={0, 0, 0, 5}, 145 | [18]={5, 2, 2, 5}, 146 | [19]={5, 4, 0, 5}, 147 | [20]={5, 4, 0, 5}, 148 | [30]={4, 0, 0, 5}, 149 | [33]={1, 0, 0, 5}, 150 | [38]={1, 0, 0, 5}, 151 | [42]={5, 4, 0, 5}, 152 | [71]={5, 4, 0, 5}, 153 | [72]={1, 4, 0, 5}, 154 | [73]={5, 4, 2, 5}, 155 | [74]={5, 4, 0, 5}, 156 | [78]={0, 4, 0, 5}, 157 | [79]={0, 4, 0, 5}, 158 | [81]={2, 0, 0, 0}, 159 | [89]={1, 4, 0, 5}, 160 | [97]={4, 4, 0, 5}, 161 | [100]={5, 0, 0, 5}, 162 | [101]={0, 4, 0, 5}, 163 | [105]={4, 0, 0, 5}, 164 | } 165 | for i=0, 127 do 166 | if picoinstr[i] == nil then 167 | picoinstr[i]={3, 0, 0, 5} 168 | end 169 | end 170 | 171 | -- Drums to PICO-8 Map 172 | local picodrum={ 173 | [35]={2, 0, 0, 5, 42}, 174 | [37]={6, 5,-1,-1, 64}, 175 | [40]={6, 0, 0, 5, 64}, 176 | [42]={6, 5,-1,-1, 90}, 177 | [53]={5, 0, 0, 5, 90}, 178 | } 179 | for i=0, 127 do 180 | if picodrum[i] == nil then 181 | picodrum[i]={6, 5,-1,-1, 84} 182 | end 183 | end 184 | 185 | -- Allowed Channels 186 | local chlisten={} 187 | for i=0, 15 do 188 | chlisten[i]=true 189 | end 190 | 191 | -- Allowed Tracks 192 | trlisten_mt={__index=function(t, k) 193 | t[k]=t.default 194 | return t.default 195 | end} 196 | local trlisten=setmetatable({default=true}, trlisten_mt) 197 | 198 | -- Drum Channels 199 | local drumch={} 200 | for i=0, 15 do 201 | drumch[i]=false 202 | end 203 | drumch[9]=true 204 | 205 | -- Per Channel Volume 206 | local chvol={} 207 | for i=0, 15 do 208 | chvol[i]=5 209 | end 210 | chvol[9]=2 211 | 212 | local maxlevel=math.huge 213 | if opts.level then 214 | maxlevel=arg2num("level") 215 | end 216 | 217 | local function log(level, ...) 218 | if level <= maxlevel then 219 | print(...) 220 | end 221 | end 222 | 223 | local function logf(level, ...) 224 | if level <= maxlevel then 225 | print(string.format(...)) 226 | end 227 | end 228 | 229 | if opts.maxvol then 230 | local maxvol=tonumber(tostring(opts.maxvol)) 231 | if not maxvol then 232 | error("Invalid value for option 'maxvol': " .. tostring(opts.maxvol), 0) 233 | elseif maxvol < 0 or maxvol > 7 then 234 | error("Invalid range for option 'maxvol': " .. maxvol, 0) 235 | end 236 | for i=0, 15 do 237 | if not drumch[i] then 238 | chvol[i]=maxvol 239 | end 240 | end 241 | end 242 | 243 | if opts.drumvol then 244 | local drumvol=tonumber(tostring(opts.drumvol)) 245 | if not drumvol then 246 | error("Invalid value for option 'drumvol': " .. tostring(opts.drumvol), 0) 247 | elseif drumvol < 0 or drumvol > 7 then 248 | error("Invalid range for option 'drumvol': " .. drumvol, 0) 249 | end 250 | for i=0, 15 do 251 | if drumch[i] then 252 | chvol[i]=drumvol 253 | end 254 | end 255 | end 256 | 257 | if opts.mute then 258 | for part in (opts.mute .. ","):gmatch("(.-),") do 259 | local chmn=tonumber(part, 10) 260 | if chmn == nil then 261 | error("Invalid channel for option 'mute': " .. part, 0) 262 | elseif chmn < -16 or chmn > 16 then 263 | error("Invalid channel for option 'mute': " .. math.abs(chmn), 0) 264 | elseif chmn == 0 then 265 | local all=(part:sub(1, 1) == "-") 266 | for i=0, 15 do 267 | chlisten[i]=all 268 | end 269 | else 270 | chlisten[math.abs(chmn)-1]=chmn < 0 271 | end 272 | end 273 | end 274 | 275 | if opts.mutet then 276 | for part in (opts.mutet .. ","):gmatch("(.-),") do 277 | local trmn=tonumber(part, 10) 278 | if trmn == nil then 279 | error("Invalid track for option 'mutet': " .. part, 0) 280 | elseif trmn < -65536 or trmn > 65536 then 281 | error("Invalid track for option 'mutet': " .. math.abs(trmn), 0) 282 | elseif trmn == 0 then 283 | trlisten=setmetatable({default=(part:sub(1, 1) == "-")}, trlisten_mt) 284 | else 285 | trlisten[math.abs(trmn)-1]=trmn < 0 286 | end 287 | end 288 | end 289 | 290 | local speed 291 | if opts.speed then 292 | speed=tonumber(tostring(opts.speed)) 293 | if not speed then 294 | error("Invalid value for option 'speed': " .. tostring(opts.speed), 0) 295 | elseif speed < 0 or speed > 255 then 296 | error("Invalid range for option 'speed': " .. speed, 0) 297 | end 298 | end 299 | 300 | if opts.chvol then 301 | for part in (opts.chvol .. ","):gmatch("(.-),") do 302 | local ch, vol=part:match("(.*):(.+)") 303 | ch=tonumber(ch, 10) 304 | vol=tonumber(vol, 10) 305 | if ch == nil or vol == nil then 306 | error("Invalid value for option 'chvol': " .. part, 0) 307 | elseif ch < 1 or ch > 16 then 308 | error("Invalid channel for option 'chvol': " .. ch, 0) 309 | elseif vol < 0 or vol > 7 then 310 | error("Invalid volume for option 'chvol': " .. vol, 0) 311 | else 312 | chvol[ch-1]=vol 313 | end 314 | end 315 | end 316 | 317 | local amodes={ 318 | blob=true, 319 | channel=true, 320 | track=true 321 | } 322 | local mode="channel" 323 | if opts.mode then 324 | if not amodes[opts.mode] then 325 | error("Invalid value for option 'mode': " .. opts.mode, 0) 326 | end 327 | mode=opts.mode 328 | end 329 | 330 | local skip=0 331 | if opts.skip then 332 | skip=arg2num("skip") 333 | end 334 | 335 | local div 336 | if opts.div then 337 | div=arg2num("div") 338 | end 339 | 340 | local shift=0 341 | if opts.shift then 342 | shift=arg2num("shift") 343 | end 344 | 345 | local dshift=0 346 | if opts.dshift then 347 | dshift=arg2num("dshift") 348 | end 349 | 350 | local text_events={ 351 | text_event=true, 352 | copyright_text_event=true, 353 | track_name=true, 354 | instrument_name=true, 355 | lyric=true, 356 | marker=true, 357 | cue_point=true, 358 | text_event_08=true, 359 | text_event_09=true, 360 | text_event_0a=true, 361 | text_event_0b=true, 362 | text_event_0c=true, 363 | text_event_0d=true, 364 | text_event_0e=true, 365 | text_event_0f=true, 366 | } 367 | local function score2note(score) 368 | local note={score[1]} 369 | local trackpos={} 370 | local nscore=#score 371 | for i=2, nscore do 372 | trackpos[i]=1 373 | end 374 | while true do 375 | local ltime, tpos=math.huge 376 | for i=2, nscore do 377 | local event=score[i][trackpos[i]] 378 | if event then 379 | local ttime=event[2] 380 | if ttime < ltime then 381 | ltime, tpos=ttime, i 382 | end 383 | end 384 | end 385 | if not tpos then break end 386 | local event=score[tpos][trackpos[tpos]] 387 | score[tpos][trackpos[tpos]]=nil 388 | trackpos[tpos]=trackpos[tpos] + 1 389 | table.insert(event, 3, tpos-2) 390 | -- Merge text events into one event type 391 | local kind=event[1] 392 | if text_events[kind] then 393 | kind=kind:gsub("_event", ""):gsub("_text", "") 394 | table.insert(event, 4, kind) 395 | event[1]="text" 396 | end 397 | note[#note+1]=event 398 | end 399 | return note 400 | end 401 | 402 | log(1, "Info: Loading and parsing midi file ...") 403 | local file, err=io.open(args[1], "rb") 404 | if not file then 405 | print(err) 406 | os.exit(1) 407 | end 408 | local data=file:read("*a") 409 | file:close() 410 | 411 | if data == nil then 412 | print("Received no data from file?") 413 | os.exit(1) 414 | end 415 | 416 | if data:sub(1, 4) ~= "MThd" then 417 | print("MIDI header missing from file") 418 | os.exit(1) 419 | end 420 | 421 | local mididata=score2note(midi.midi2score(data)) 422 | 423 | local function gcd(m, n) 424 | while m ~= 0 do 425 | m, n=n%m, m 426 | end 427 | return n 428 | end 429 | 430 | -- MIDI timing analytics 431 | local commondiv={} 432 | for i=1, 30 do 433 | commondiv[i*5]=0 434 | commondiv[i*6]=0 435 | end 436 | 437 | local timediff={} 438 | local lnote=0 439 | 440 | if opts.analysis and not opts.ignorediv then 441 | log(2, "Warning: --analysis implies --ignorediv") 442 | opts.ignorediv=true 443 | end 444 | 445 | if not div then 446 | log(1, "Info: Attempting to detect time division ...") 447 | for i=2, #mididata do 448 | local event=mididata[i] 449 | if event[1] == "note" and chlisten[event[5]] and trlisten[event[3]] then 450 | local time=event[2]-skip 451 | if time > 0 then 452 | if opts.analysis and time-lnote > 0 then 453 | timediff[time-lnote]=(timediff[time-lnote] or 0)+1 454 | lnote=time 455 | end 456 | if not div then 457 | div=time 458 | commondiv[div]=1 459 | if div == 1 then 460 | print("\nError: Failed to detect time division! First note starts at 1") 461 | os.exit(1) 462 | end 463 | else 464 | local ldiv=div 465 | div=math.min(div, gcd(div, time)) 466 | if opts.analysis then 467 | if not commondiv[div] then 468 | commondiv[div]=1 469 | else 470 | local highest=-math.huge 471 | for k, v in pairs(commondiv) do 472 | if time/k == math.floor(time/k) or (opts.fixdivone and ((time-1)/k == math.floor((time-1)/k) or (time+1)/k == math.floor((time+1)/k))) then 473 | highest=math.max(highest, k) 474 | end 475 | end 476 | commondiv[highest]=commondiv[highest]+1 477 | end 478 | end 479 | if ldiv ~= div and opts.fixdivone then 480 | if math.min(ldiv, gcd(ldiv, time-1)) == ldiv then 481 | div=ldiv 482 | logf(2, "Warning: Corrected off by one error at %d/%d (-1)", time, div) 483 | elseif math.min(ldiv, gcd(ldiv, time+1)) == ldiv then 484 | div=ldiv 485 | logf(2, "Warning: Corrected off by one error at %d/%d (+1)", time, div) 486 | end 487 | end 488 | if div == 1 then 489 | local bad=time/ldiv 490 | if not opts.ignorediv then 491 | print("\nError: Failed to detect time division!") 492 | print("Last good division was "..ldiv) 493 | print("Bad note at "..time.."/"..ldiv.." = "..bad.." ("..math.floor(bad).." + "..time-math.floor(bad)*ldiv.."/"..ldiv..")") 494 | print("\nTry using --analysis for suggestions") 495 | print("--ignorediv and --fixdivone may help correct errors") 496 | os.exit(1) 497 | else 498 | log(2, "Warning: Ignoring bad time division of "..time.."/"..ldiv.." = "..bad) 499 | div=ldiv 500 | end 501 | end 502 | end 503 | end 504 | end 505 | end 506 | if not div then 507 | print("\nError: Failed to detect time division! No notes?", 0) 508 | os.exit(1) 509 | end 510 | log(1, "Info: Detected: " .. div) 511 | end 512 | 513 | local function process(tbl, message) 514 | print(message) 515 | local high=-math.huge 516 | local highk=-math.huge 517 | local sorttbl={} 518 | for k, v in pairs(tbl) do 519 | if v > high then 520 | highk, high=k, v 521 | elseif v == high then 522 | highk=math.max(highk, k) 523 | end 524 | sorttbl[#sorttbl+1]={k, v} 525 | end 526 | table.sort(sorttbl, function(a, b) return a[2] 0 then 574 | local msg=(factor ~= 1) and "Other choices" or "Possibly try" 575 | print(msg..": "..table.concat(try, ", ")) 576 | else 577 | print("No suggestions available, look at above lists") 578 | end 579 | 580 | os.exit(1) 581 | end 582 | 583 | if not speed then 584 | local ppq=mididata[1] 585 | log(1, "Info: Attempting to detect speed ...") 586 | local tempo 587 | local warned=false 588 | local notes=false 589 | for i=2, #mididata do 590 | local event=mididata[i] 591 | if event[1] == "set_tempo" then 592 | if not tempo or event[2] == 0 or not notes then 593 | tempo=event[4] 594 | elseif tempo ~= event[4] and not warned then 595 | log(1, "Info: midi changes tempo mid song, this is currently not supported.") 596 | warned=true 597 | end 598 | elseif event[1] == "note" then 599 | notes=true 600 | end 601 | end 602 | if not tempo then 603 | log(1, "Info: No tempo events, using default of 500000") 604 | tempo=500000 605 | end 606 | local fspeed=div*(tempo/1000/ppq)/(1220/147) 607 | speed=math.max(math.round(fspeed), 1) 608 | logf(1, "Info: Detected: %s (%d)", fspeed, speed) 609 | end 610 | 611 | local function note2pico(note, drum) 612 | local val=(note-36)+(drum and dshift or shift) 613 | local msg=(drum and "Drum note" or "Note") 614 | if val > 63 then 615 | logf(2, "Warning: %s too high, truncating: %d, %+d", msg, val, val-63) 616 | val=63 617 | end 618 | if val < 0 then 619 | logf(2, "Warning: %s too low, truncating: %d", msg, val) 620 | val=0 621 | end 622 | return val 623 | end 624 | 625 | local slice={} 626 | local function getChunk(i) 627 | if not slice[i] then 628 | slice[i]={} 629 | end 630 | return slice[i] 631 | end 632 | 633 | -- Configured Midi Information 634 | local vol={} 635 | local expr={} 636 | local prgm={} 637 | local pwheel={} 638 | local nrpn={} 639 | local rpn={} 640 | local nrpns={} 641 | local rpns={} 642 | local lrpn 643 | local function resetmidi() 644 | for i=0, 15 do 645 | vol[i]=127 646 | expr[i]=127 647 | prgm[i]=0 648 | pwheel[i]=0 649 | nrpn[i]={[0]=2} 650 | rpn[i]={[0]=2} 651 | nrpns[i]=0 652 | rpns[i]=0 653 | end 654 | lrpn=nil 655 | end 656 | resetmidi() 657 | 658 | local mtime=-math.huge 659 | local stime=math.huge 660 | 661 | local function parseevent(event) 662 | if event[1] == "note" and chlisten[event[5]] and trlisten[event[3]] and event[2]-skip >= 0 then 663 | event[2]=event[2]-skip 664 | if event[2]/div ~= math.floor(event[2]/div) then 665 | log(2, "Invalid division: " .. event[2] .. " -> " .. event[2]/div) 666 | end 667 | local time=math.floor(event[2]/div) 668 | mtime=math.max(mtime, time) 669 | stime=math.min(stime, time) 670 | local chunk=getChunk(time) 671 | local chunkdata={note=event[6], vol=vol[event[5]], expr=expr[event[5]], vel=event[7], prgm=prgm[event[5]], pwheel=pwheel[event[5]]/8192*rpn[event[5]][0], ch=event[5], durat=event[4]} 672 | if drumch[event[5]] then 673 | chunkdata.prgm=chunkdata.note 674 | chunkdata.note=picodrum[chunkdata.prgm][5] 675 | end 676 | local placed=false 677 | if mode == "blob" then 678 | if #chunk < 4 then 679 | chunk[#chunk+1]=chunkdata 680 | placed=true 681 | end 682 | elseif mode == "channel" then 683 | local cpos=(event[5]%4)+1 684 | if chunk[cpos] == nil then 685 | chunk[cpos]=chunkdata 686 | placed=true 687 | end 688 | elseif mode == "track" then 689 | local tpos=((event[3]-1)%4)+1 690 | if chunk[tpos] == nil then 691 | chunk[tpos]=chunkdata 692 | placed=true 693 | end 694 | end 695 | if not placed and mode ~= "blob" then 696 | for i=1, 4 do 697 | if chunk[i] == nil then 698 | chunk[i]=chunkdata 699 | placed=true 700 | break 701 | end 702 | end 703 | end 704 | if not placed then 705 | log(2, "Warning: Overran " .. time) 706 | local kill 707 | local note=event[6] 708 | for i=1, 4 do 709 | if event[6] > chunk[i].note then 710 | kill=i 711 | end 712 | end 713 | if kill then 714 | chunk[kill]=chunkdata 715 | end 716 | end 717 | elseif event[1] == "text" then 718 | log(1, "Info: (Text) " .. event[4] .. ": " .. event[5]) 719 | elseif event[1] == "control_change" then 720 | if event[5] == 0 then 721 | -- No Banks. 722 | elseif event[5] == 6 then 723 | if lrpn == true then 724 | rpn[event[4]][rpns[event[4]]]=event[6] 725 | elseif lrpn == false then 726 | nrpn[event[4]][nrpns[event[4]]]=event[6] 727 | end 728 | lrpn=nil 729 | elseif event[5] == 7 then 730 | vol[event[4]]=event[6] 731 | elseif event[5] == 8 or event[5] == 10 then 732 | -- No Balance/Panning. 733 | if event[6] ~= 64 then 734 | log(2, "Warning: " .. (event[5] == 8 and "balance" or "panning") .. " (ch:" .. event[4] .. "=" .. (event[6]-64) .. ") is not supported.") 735 | end 736 | elseif event[5] == 11 then 737 | expr[event[4]]=event[6] 738 | elseif event[5] == 98 then 739 | nrpns[event[4]]=bit.bor(bit.band(nrpns[event[4]], 0x3f80), event[6]) 740 | lrpn=false 741 | elseif event[5] == 99 then 742 | nrpns[event[4]]=bit.bor(bit.band(nrpns[event[4]], 0x7f), bit.lshift(event[6], 7)) 743 | lrpn=false 744 | elseif event[5] == 100 then 745 | rpns[event[4]]=bit.bor(bit.band(rpns[event[4]], 0x3f80), event[6]) 746 | lrpn=true 747 | elseif event[5] == 101 then 748 | rpns[event[4]]=bit.bor(bit.band(rpns[event[4]], 0x7f), bit.lshift(event[6], 7)) 749 | lrpn=true 750 | else 751 | local time, track, channel, control, value=table.unpack(event, 2) 752 | channel=channel+1 753 | logf(2, "Warning: Unknown Controller: {%d, T%d, CH%d, CC%d, V%d}", time, track, channel, control, value) 754 | end 755 | elseif event[1] == "patch_change" then 756 | prgm[event[4]]=event[5] 757 | elseif event[1] == "pitch_wheel_change" then 758 | if not drumch[event[4]] then 759 | pwheel[event[4]]=event[5] 760 | else 761 | log(2, "Warning: Ignoring pitch wheel event on drum channel: " .. event[4]) 762 | end 763 | else 764 | 765 | end 766 | end 767 | for i=2, #mididata do 768 | local event=mididata[i] 769 | local ok, err=pcall(parseevent, event) 770 | if not ok then 771 | io.stderr:write("Crashed parsing event : {" .. table.concat(event, ", ") .. "}\n\n" .. err .. "\n") 772 | os.exit(1) 773 | end 774 | end 775 | 776 | log(1, "Info: Extending notes ...") 777 | local cpparm={"note", "vol", "expr", "vel", "prgm", "pwheel", "ch"} 778 | local lostnotes=0 779 | for i=0, mtime do 780 | if slice[i] then 781 | local chunk=slice[i] 782 | for j=1, 4 do 783 | if chunk[j] and chunk[j].durat then 784 | local kstop=math.ceil(chunk[j].durat/div)-1 785 | for k=1, kstop do 786 | mtime=math.max(mtime, i+k) 787 | local chunk2=getChunk(i+k) 788 | if not chunk2[j] then 789 | chunk2[j]={} 790 | end 791 | if not chunk2[j].note then 792 | for i=1, #cpparm do 793 | chunk2[j][cpparm[i]]=chunk[j][cpparm[i]] 794 | end 795 | chunk2[j].pos=((k == kstop) and "E" or "M") 796 | else 797 | local lost=kstop - k + 1 798 | local lchunk=slice[i+k-1][j] 799 | if k > 1 then 800 | lchunk.pos="E" 801 | end 802 | logf(2, "Warning: Note blocking Note, lost %d", lost) 803 | lchunk.lost=lost 804 | lostnotes=lostnotes+lost 805 | break 806 | end 807 | end 808 | chunk[j].durat=nil 809 | chunk[j].pos="S" 810 | end 811 | end 812 | end 813 | end 814 | if lostnotes > 0 then 815 | logf(1, "Info: Lost %d notes", lostnotes) 816 | end 817 | if lostnotes > 0 and not opts.noregain then 818 | local regained=0 819 | log(1, "Info: Attempting to regain notes ...") 820 | for i=0, mtime do 821 | if slice[i] then 822 | local chunk=slice[i] 823 | for j=1, 4 do 824 | local schunk=chunk[j] 825 | if schunk and schunk.lost then 826 | local chunk2=getChunk(i+1) 827 | local tj 828 | for k=1, 4 do 829 | if not chunk2[k] or not chunk2[k].note then 830 | tj=k 831 | break 832 | end 833 | end 834 | if tj then 835 | for k=1, schunk.lost do 836 | mtime=math.max(mtime, i+k) 837 | local chunk2=getChunk(i+k) 838 | if not chunk2[tj] then 839 | chunk2[tj]={} 840 | end 841 | if not chunk2[tj].note then 842 | for i=1, #cpparm do 843 | chunk2[tj][cpparm[i]]=schunk[cpparm[i]] 844 | end 845 | chunk2[tj].pos=((k == schunk.lost) and "E" or "M") 846 | if k == schunk.lost then 847 | logf(2, "Warning: Regained %d notes", schunk.lost) 848 | regained=regained+schunk.lost 849 | end 850 | else 851 | local lost=schunk.lost - k + 1 852 | local lchunk=slice[i+k-1][tj] 853 | if k > 1 then 854 | lchunk.pos="E" 855 | end 856 | logf(2, "Warning: Regained %d notes", k-1) 857 | lchunk.lost=lost 858 | regained=regained+k-1 859 | break 860 | end 861 | end 862 | schunk.lost=nil 863 | schunk.pos="M" 864 | end 865 | end 866 | end 867 | end 868 | end 869 | logf(1, "Info: Regained %d notes", regained) 870 | if lostnotes == regained then 871 | log(1, "Info: Regained all notes back!") 872 | end 873 | end 874 | if not opts.no2ndpass then 875 | log(1, "Info: Performing second corrective pass ...") 876 | resetmidi() 877 | local pass2nd={} 878 | local function parseevent2(event) 879 | if event[1] == "control_change" then 880 | if event[5] == 6 then 881 | if lrpn == true then 882 | rpn[event[4]][rpns[event[4]]]=event[6] 883 | if rpns[event[4]] == 0 then 884 | local time=math.floor(event[2]/div) 885 | if not pass2nd[time] then 886 | pass2nd[time]={} 887 | end 888 | local chunk=pass2nd[time] 889 | if not chunk.pwheel then 890 | chunk.pwheel={} 891 | end 892 | chunk.pwheel[event[4]]=pwheel[event[4]]/8192*event[6] 893 | end 894 | elseif lrpn == false then 895 | nrpn[event[4]][nrpns[event[4]]]=event[6] 896 | end 897 | lrpn=nil 898 | elseif event[5] == 7 then 899 | local time=math.floor(event[2]/div) 900 | if not pass2nd[time] then 901 | pass2nd[time]={} 902 | end 903 | local chunk=pass2nd[time] 904 | if not chunk.vol then 905 | chunk.vol={} 906 | end 907 | chunk.vol[event[4]]=event[6] 908 | elseif event[5] == 11 then 909 | local time=math.floor(event[2]/div) 910 | if not pass2nd[time] then 911 | pass2nd[time]={} 912 | end 913 | local chunk=pass2nd[time] 914 | if not chunk.expr then 915 | chunk.expr={} 916 | end 917 | chunk.expr[event[4]]=event[6] 918 | elseif event[5] == 98 then 919 | nrpns[event[4]]=bit.bor(bit.band(nrpns[event[4]], 0x3f80), event[6]) 920 | lrpn=false 921 | elseif event[5] == 99 then 922 | nrpns[event[4]]=bit.bor(bit.band(nrpns[event[4]], 0x7f), bit.lshift(event[6], 7)) 923 | lrpn=false 924 | elseif event[5] == 100 then 925 | rpns[event[4]]=bit.bor(bit.band(rpns[event[4]], 0x3f80), event[6]) 926 | lrpn=true 927 | elseif event[5] == 101 then 928 | rpns[event[4]]=bit.bor(bit.band(rpns[event[4]], 0x7f), bit.lshift(event[6], 7)) 929 | lrpn=true 930 | end 931 | elseif event[1] == "patch_change" then 932 | prgm[event[4]]=event[5] 933 | elseif event[1] == "pitch_wheel_change" then 934 | pwheel[event[4]]=event[5] 935 | local time=math.floor(event[2]/div) 936 | if not pass2nd[time] then 937 | pass2nd[time]={} 938 | end 939 | local chunk=pass2nd[time] 940 | if not chunk.pwheel then 941 | chunk.pwheel={} 942 | end 943 | chunk.pwheel[event[4]]=event[5]/8192*rpn[event[4]][0] 944 | end 945 | end 946 | for i=2, #mididata do 947 | local event=mididata[i] 948 | local ok, err=pcall(parseevent2, event) 949 | if not ok then 950 | io.stderr:write("Crashed parsing event : {" .. table.concat(event, ", ") .. "}\n\n" .. err .. "\n") 951 | os.exit(1) 952 | end 953 | end 954 | do 955 | local vol={} 956 | local expr={} 957 | local pwheel={} 958 | for i=0, 15 do 959 | vol[i]=127 960 | expr[i]=127 961 | pwheel[i]=0 962 | end 963 | for i=0, mtime do 964 | if pass2nd[i] then 965 | local vold=pass2nd[i].vol 966 | local exprd=pass2nd[i].expr 967 | local pwheeld=pass2nd[i].pwheel 968 | if vold then 969 | for i=0, 15 do 970 | if vold[i] then vol[i]=vold[i] end 971 | end 972 | end 973 | if exprd then 974 | for i=0, 15 do 975 | if exprd[i] then expr[i]=exprd[i] end 976 | end 977 | end 978 | if pwheeld then 979 | for i=0, 15 do 980 | if pwheeld[i] then pwheel[i]=pwheeld[i] end 981 | end 982 | end 983 | end 984 | local chunk=slice[i] 985 | if chunk then 986 | for j=1, 4 do 987 | if chunk[j] then 988 | local schunk=chunk[j] 989 | if vol[schunk.ch] ~= schunk.vol and not opts.novol then 990 | logf(2, "Warning: Corrected volume from %s to %s", schunk.vol, vol[schunk.ch]) 991 | end 992 | schunk.vol=vol[schunk.ch] 993 | if expr[schunk.ch] ~= schunk.expr and not opts.noexpr then 994 | logf(2, "Warning: Corrected expression from %s to %s", schunk.expr, expr[schunk.ch]) 995 | end 996 | schunk.expr=expr[schunk.ch] 997 | if pwheel[schunk.ch] ~= schunk.pwheel and not opts.nopwheel then 998 | logf(2, "Warning: Corrected pitch wheel from %s to %s", schunk.pwheel, pwheel[schunk.ch]) 999 | end 1000 | schunk.pwheel=pwheel[schunk.ch] 1001 | end 1002 | end 1003 | end 1004 | end 1005 | end 1006 | end 1007 | if stime ~= 0 then 1008 | log(1, "Info: Trimming " .. stime .. " slices ...") 1009 | for i=stime, mtime+stime do 1010 | slice[i-stime]=slice[i] 1011 | end 1012 | mtime=mtime-stime 1013 | stime=0 1014 | end 1015 | local pats=math.ceil(mtime/32)-1 1016 | log(1, "Info: " .. pats+1 .. " patterns") 1017 | local outfile, err 1018 | if args[2] then 1019 | log(1, "Info: Writing to '" .. args[2] .. "'") 1020 | outfile, err=io.open(args[2], "wb") 1021 | if not outfile then 1022 | error(err, 0) 1023 | end 1024 | else 1025 | log(1, "Info: Writing to stdout") 1026 | outfile=io.stdout 1027 | end 1028 | if not opts.musichax then 1029 | outfile:write([[pico-8 cartridge // http://www.pico-8.com 1030 | version 8 1031 | ]]) 1032 | if opts.stub then 1033 | outfile:write([[__lua__ 1034 | music(0) 1035 | function _update() end 1036 | ]]) 1037 | end 1038 | outfile:write("__sfx__\n") 1039 | end 1040 | local base=0 1041 | local patsel={} 1042 | local linemap={} 1043 | local kill={} 1044 | local count=0 1045 | 1046 | -- for musichax 1047 | local sfxdata 1048 | local cartdata 1049 | if not opts.musichax then 1050 | linemap[string.format("01%02x0000", tonumber(speed))..string.rep("0", 32*5)]=-1 -- don't emit empty pattern. 1051 | else 1052 | linemap[string.rep("\0", 64)]=-1 -- don't emit empty pattern. 1053 | sfxdata="" 1054 | end 1055 | for block=0, pats*32, 32 do 1056 | local top=0 1057 | for i=0, 31 do 1058 | local chunk=getChunk(i+block) 1059 | for j=1, 4 do 1060 | if chunk[j] and chunk[j].note then 1061 | top=math.max(top, j) 1062 | end 1063 | end 1064 | if top == 4 then 1065 | break 1066 | end 1067 | end 1068 | for j=1, top do 1069 | local line, empty 1070 | if not opts.musichax then 1071 | line=string.format("01%02x0000", tonumber(speed)) 1072 | empty="00000" 1073 | else 1074 | line="" 1075 | empty="\0\0" 1076 | end 1077 | for i=0, 31 do 1078 | local chunk=getChunk(i+block) 1079 | if chunk[j] and chunk[j].note then 1080 | local info=chunk[j] 1081 | if opts.nopwheel then 1082 | info.pwheel=0 1083 | end 1084 | if opts.noexpr then 1085 | info.expr=127 1086 | end 1087 | if opts.novol then 1088 | info.vol=127 1089 | info.vel=127 1090 | info.expr=127 1091 | end 1092 | local instr=info.prgm 1093 | local drum=drumch[info.ch] 1094 | local val=note2pico(math.floor(info.note+info.pwheel+0.5), drum) 1095 | if val <= 63 then 1096 | local place=3 1097 | if info.pos == "S" then 1098 | place=2 1099 | elseif info.pos == "E" then 1100 | place=4 1101 | end 1102 | local instrdata=drum and picodrum[instr] or picoinstr[instr] 1103 | if instrdata[place] ~= -1 then 1104 | local note, instr, vol, fx=val, instrdata[1], math.floor((info.vol/127)*(info.vel/127)*(info.expr/127)*(chvol[info.ch]-1)+1.5), instrdata[place] 1105 | if not opts.musichax then 1106 | line=line .. string.format("%02x%x%s%x", note, instr, vol, fx) 1107 | else 1108 | local combo=(note*(2^0))+(instr*(2^6))+(vol*(2^9))+(fx*(2^12)) 1109 | line=line .. string.char(combo%256, math.floor(combo/256)) 1110 | end 1111 | else 1112 | line=line .. empty 1113 | end 1114 | else 1115 | log(2, "Dropping high pitched note.") 1116 | line=line .. empty 1117 | end 1118 | else 1119 | line=line .. empty 1120 | end 1121 | end 1122 | if not linemap[line] then 1123 | linemap[line]=base+j-1 1124 | if not opts.musichax then 1125 | if count >= 64 and not opts.notrunc then 1126 | outfile:close() 1127 | error("Midi is too long or time division is too short.\nUse --notrunc to continue writing.", 0) 1128 | end 1129 | outfile:write(line..(opts.debug and string.format(" %02x", count) or "").."\n") 1130 | else 1131 | sfxdata=sfxdata..line 1132 | if #sfxdata/64 >= 256 then 1133 | error("Too much sfx data", 0) 1134 | end 1135 | end 1136 | count=count+1 1137 | else 1138 | linemap[base+j-1]=linemap[line] 1139 | kill[#kill+1]=base+j-1 1140 | end 1141 | end 1142 | local patblock={} 1143 | for i=0, top-1 do 1144 | patblock[#patblock+1]=linemap[base+i] or base+i 1145 | end 1146 | base=base+top 1147 | patsel[block/32]=patblock 1148 | end 1149 | for block=0, pats do 1150 | local patblock=patsel[block] 1151 | for i=1, #patblock do 1152 | local val=patblock[i] 1153 | local subtract=0 1154 | for i=1, #kill do 1155 | if kill[i] <= val then 1156 | subtract=subtract+1 1157 | else 1158 | break 1159 | end 1160 | end 1161 | patblock[i]=val-subtract 1162 | end 1163 | end 1164 | if not opts.musichax then 1165 | outfile:write("__music__\n") 1166 | end 1167 | local first=true 1168 | local firstpat 1169 | for block=0, pats do 1170 | local line 1171 | if opts.musichax then 1172 | line="" 1173 | elseif first then 1174 | line="01 " 1175 | elseif block == pats then 1176 | line="02 " 1177 | else 1178 | line="00 " 1179 | end 1180 | local patblock=patsel[block] 1181 | if not opts.musichax then 1182 | for i=1, 4 do 1183 | if patblock[i] and patblock[i] >= 0x40 then 1184 | if opts.notrunc then 1185 | logf(2, "Warning: Ran out of sfx: %d, (%02x)", patblock[i], patblock[i]) 1186 | else 1187 | outfile:close() 1188 | error("Midi is too long or time division is too short.\nUse --notrunc to continue writing.", 0) 1189 | end 1190 | end 1191 | if not patblock[i] or patblock[i] == -1 then 1192 | patblock[i]=0x40 1193 | elseif patblock[i] >= 0x40 then 1194 | patblock[i]=0x40 1195 | end 1196 | patblock[i]=string.format("%02x", patblock[i]) 1197 | end 1198 | local pattern=table.concat(patblock, "") 1199 | if not first or pattern ~= "40404040" then 1200 | first=false 1201 | outfile:write(line .. table.concat(patblock, "")..(opts.debug and " "..block or "").."\n") 1202 | end 1203 | else 1204 | for i=1, 4 do 1205 | if not patblock[i] or patblock[i] == -1 then 1206 | patblock[i]=0xFF 1207 | end 1208 | end 1209 | local pattern=string.char(patblock[1], patblock[2], patblock[3], patblock[4]) 1210 | if not first or pattern ~= "\255\255\255\255" then 1211 | if first then 1212 | if pats-block >= 256 then 1213 | error("Too many patterns", 0) 1214 | end 1215 | cartdata=string.char(pats-block, #sfxdata/64-1, 0, pats-block) -- number of patterns, number of sfx, loop start, loop end 1216 | firstpat=block 1217 | end 1218 | first=false 1219 | cartdata=cartdata..pattern 1220 | end 1221 | end 1222 | end 1223 | if opts.musichax then 1224 | cartdata=cartdata..sfxdata 1225 | local padding=0x4300-4-((pats-firstpat+1)*4)-#sfxdata-(68*4) 1226 | if padding < 0 then 1227 | error("too much data for MusicHAX") 1228 | end 1229 | cartdata=cartdata..string.rep("\0", padding) 1230 | for i=1, 4 do 1231 | cartdata=cartdata..string.rep("\0", 64).."\1"..string.char(speed).."\0\32" 1232 | end 1233 | local mhsfile, err=io.open("musichax-stub.lua", "rb") 1234 | if not mhsfile then 1235 | error(err, 0) 1236 | end 1237 | local code=mhsfile:read("*a") 1238 | mhsfile:close() 1239 | 1240 | -- write cart 1241 | local bin2hex=function(a) return ("%02x"):format(a:byte()) end 1242 | outfile:write([[pico-8 cartridge // http://www.pico-8.com 1243 | version 8 1244 | __lua__ 1245 | ]]..code.."\n__gfx__\n") 1246 | for i=0, 0x1fff, 64 do 1247 | outfile:write(cartdata:sub(i+1, i+64):gsub(".", function(a) a=a:byte() return string.format("%02x", bit.bor(bit.lshift(bit.band(a, 0x0f), 4), bit.rshift(bit.band(a, 0xf0), 4))) end).."\n") 1248 | end 1249 | outfile:write("__gff__\n") 1250 | for i=0x3000, 0x30ff, 128 do 1251 | outfile:write(cartdata:sub(i+1, i+128):gsub(".", bin2hex).."\n") 1252 | end 1253 | outfile:write("__map__\n") 1254 | for i=0x2000, 0x2fff, 128 do 1255 | outfile:write(cartdata:sub(i+1, i+128):gsub(".", bin2hex).."\n") 1256 | end 1257 | outfile:write("__sfx__\n") 1258 | for i=0x3200, 0x42ff, 68 do 1259 | local sfx=cartdata:sub(i+65, i+68):gsub(".", bin2hex) 1260 | local notes=cartdata:sub(i+1, i+64):gsub("..", function(a) 1261 | local l, h=a:byte(1, -1) 1262 | a=bit.bor(bit.lshift(h, 8), l) 1263 | local note=bit.band(a, 0x003f) 1264 | local instr=bit.rshift(bit.band(a, 0x01c0), 6) 1265 | local vol=bit.rshift(bit.band(a, 0x0e00), 9) 1266 | local fx=bit.rshift(bit.band(a, 0x7000), 12) 1267 | return string.format("%02x%x%s%x", note, instr, vol, fx) 1268 | end) 1269 | outfile:write(sfx..notes.."\n") 1270 | end 1271 | outfile:write("__music__\n") 1272 | for i=0x3100, 0x31ff, 4 do 1273 | local loop=0 1274 | local chn={cartdata:byte(i+1, i+4)} 1275 | for j=0, 3 do 1276 | loop=bit.bor(loop, bit.lshift(bit.band(chn[j+1], 0x80) ~= 0 and 1 or 0, j)) 1277 | chn[j+1]=bit.band(chn[j+1], 0x7f) 1278 | end 1279 | outfile:write(("%02x %02x%02x%02x%02x\n"):format(loop, chn[1], chn[2], chn[3], chn[4])) 1280 | end 1281 | outfile:write("\n") 1282 | end 1283 | if args[2] then 1284 | outfile:close() 1285 | end 1286 | log(1, "Info: Finished!") 1287 | -------------------------------------------------------------------------------- /musichax-stub.lua: -------------------------------------------------------------------------------- 1 | local npat=peek(0)+1 2 | local nsfx=peek(1)+1 3 | local lstr=peek(2)+1 4 | local lend=peek(3)+1 5 | local base=4+npat*4 6 | local pats={} 7 | local lpos=0 8 | local cpat=1 9 | local function cpdata(i, len) 10 | local pat=peek(i+cpat*4) 11 | local addr=0x41f0+i*68+lpos*2 12 | if pat ~= 255 then 13 | memcpy(addr, pat*64+base+lpos*2, len) 14 | else 15 | memset(addr, 0, len) 16 | end 17 | return pat 18 | end 19 | for i=0,3 do 20 | pats[i]=cpdata(i, 64) 21 | if (pats[i] == 255) pats[i]=-1 22 | end 23 | cpat=2 24 | local lcpt=1 25 | sfx(60, 0) 26 | sfx(61, 1) 27 | sfx(62, 2) 28 | sfx(63, 3) 29 | 30 | local function updmusic() 31 | local pos=stat(20) 32 | if pos < lpos then 33 | for i=0, 3 do 34 | local pat=cpdata(i, 64-lpos*2) 35 | if (pat == 255) pat=-1 36 | pats[i]=pat 37 | end 38 | lcpt=cpat 39 | if cpat==lend then 40 | cpat=lstr 41 | else 42 | cpat+=1 43 | end 44 | lpos=0 45 | for i=0, 3 do 46 | cpdata(i, pos*2) 47 | end 48 | else 49 | for i=0, 3 do 50 | cpdata(i, (pos-lpos)*2) 51 | end 52 | end 53 | lpos=pos 54 | end 55 | 56 | function _init() 57 | cls() 58 | print(npat.." patterns") 59 | print(nsfx.." sfx data") 60 | print("loop start: "..(lstr-1)) 61 | print("loop end: "..(lend-1)) 62 | print("") 63 | print("streaming music ...") 64 | end 65 | 66 | function _update60() 67 | updmusic() 68 | end 69 | 70 | function _draw() 71 | rectfill(0, 42, 35, 47, 0) 72 | print("["..(lcpt-1).."/"..(npat-1).."]", 0, 42, 6) 73 | rectfill(0, 54, 23, 107, 0) 74 | for i=0, 3 do 75 | local val=pats[i] 76 | color(val ~= -1 and 6 or 5) 77 | print(i..") "..pats[i], 0, (i+9)*6) 78 | end 79 | for i=20, 23 do 80 | local val=stat(i) 81 | color(val ~= -1 and 6 or 5) 82 | print((i-20)..") "..val, 0, (i-6)*6) 83 | end 84 | end 85 | --------------------------------------------------------------------------------