Advertisement
Alakazard12

MID

Jan 7th, 2014
208
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 10.14 KB | None | 0 0
  1. local args, options = shell.parse(...)
  2. if #args < 1 then
  3.   print("Usage: midi [-i] <filename> [track1 [track2 [...]]]")
  4.   print("Where the tracks are the numbers of the tracks to actually play.")
  5.   print(" -i: only parse and show track info, don't play.")
  6.   return
  7. end
  8.  
  9. -- Implements the essentials for MIDI file parsing, references:
  10. -- http://www.recordingblogs.com/sa/tabid/88/Default.aspx?topic=Musical+Instrument+Digital+Interface+(MIDI)
  11. -- http://www.sonicspot.com/guide/midifiles.html
  12.  
  13. local enabledTracks = {n=0}
  14. for i = 2, #args do
  15.   enabledTracks[tonumber(args[i])] = true
  16.   enabledTracks.n = enabledTracks.n + 1
  17. end
  18.  
  19. local instruments = {}
  20. for address in component.list("note_block") do
  21.   table.insert(instruments, component.proxy(address))
  22. end
  23. if #instruments == 0 then
  24.   print("no note blocks found")
  25.   return
  26. end
  27.  
  28. print("Using " .. #instruments .. " note blocks.")
  29.  
  30. local filename = shell.resolve(args[1])
  31. local f, reason = io.open(filename, "rb")
  32. if not f then
  33.   print(reason)
  34.   return
  35. end
  36.  
  37. local function parseVarInt(s, bits) -- parses multiple bytes as an integer
  38.   if not s then
  39.     error("error parsing file")
  40.   end
  41.   bits = bits or 8
  42.   local mask = bit32.rshift(0xFF, 8 - bits)
  43.   local num = 0
  44.   for i = 1, s:len() do
  45.     num = num + bit32.lshift(bit32.band(s:byte(i), mask), (s:len() - i) * bits)
  46.   end
  47.   return num
  48. end
  49.  
  50. local function readChunkInfo() -- reads chunk header info
  51.   local id = f:read(4)
  52.   if not id then
  53.     return
  54.   end
  55.   return id, parseVarInt(f:read(4))
  56. end
  57.  
  58. -- Read the file header and with if file information.
  59. local id, size = readChunkInfo()
  60. if id ~= "MThd" or size ~= 6 then
  61.   print("error parsing header (" .. id .. "/" .. size .. ")")
  62.   return
  63. end
  64.  
  65. local format = parseVarInt(f:read(2))
  66. local tracks = parseVarInt(f:read(2))
  67. local delta = parseVarInt(f:read(2))
  68.  
  69. if format < 0 or format > 2 then
  70.   print("unknown format")
  71.   return
  72. end
  73.  
  74. local formatName = ({"single", "synchronous", "asynchronous"})[format + 1]
  75. print(string.format("Found %d %s tracks.", tracks, formatName))
  76.  
  77. if format == 2 then
  78.   print("Sorry, asynchronous tracks are not supported.")
  79.   return
  80. end
  81.  
  82. -- Figure out our time system and prepare accordingly.
  83. local time = {division = bit32.band(0x8000, delta) == 0 and "tpb" or "fps"}
  84. if time.division == "tpb" then
  85.   time.tpb = bit32.band(0x7FFF, delta)
  86.   time.mspb = 500000
  87.   function time.tick()
  88.     return time.mspb / time.tpb
  89.   end
  90.   print(string.format("Time division is in %d ticks per beat.", time.tpb))
  91. else
  92.   time.fps = bit32.band(0x7F00, delta)
  93.   time.tpf = bit32.band(0x00FF, delta)
  94.   function time.tick()
  95.     return 1000000 / (time.fps * time.tpf)
  96.   end
  97.   print(string.format("Time division is in %d frames per second with %d ticks per frame.", time.fps, time.tpf))
  98. end
  99.  
  100. -- Parse all track chunks.
  101. local totalOffset = 0
  102. local totalLength = 0
  103. local tracks = {}
  104. while true do
  105.   local id, size = readChunkInfo()
  106.   if not id then
  107.     break
  108.   end
  109.   if id == "MTrk" then
  110.     local track = {}
  111.     local cursor = 0
  112.     local start, offset = f:seek(), 0
  113.     local inSysEx = false
  114.     local running = 0
  115.  
  116.     local function read(n)
  117.       n = n or 1
  118.       if n > 0 then
  119.         offset = offset + n
  120.         return f:read(n)
  121.       end
  122.     end
  123.     local function readVariableLength()
  124.       local total = ""
  125.       for i = 1, math.huge do
  126.         local part = read()
  127.         total = total .. part
  128.         if bit32.band(0x80, part:byte(1)) == 0 then
  129.           return parseVarInt(total, 7)
  130.         end
  131.       end
  132.     end
  133.     local function parseVoiceMessage(event)
  134.       local channel = bit32.band(0xF, event)
  135.       local note = parseVarInt(read())
  136.       local velocity = parseVarInt(read())
  137.       return channel, note, velocity
  138.     end
  139.  
  140.     while offset < size do
  141.       cursor = cursor + readVariableLength()
  142.       totalLength = math.max(totalLength, cursor)
  143.       local test = parseVarInt(read())
  144.       if inSysEx and test ~= 0xF7 then
  145.         error("corrupt file: could not find continuation of divided sysex event")
  146.       end
  147.       local event
  148.       if bit32.band(test, 0x80) == 0 then
  149.         if running == 0 then
  150.           error("corrupt file: invalid running status")
  151.         end
  152.         f.buffer = string.char(test) .. f.buffer
  153.         offset = offset - 1
  154.         event = running
  155.       else
  156.         event = test
  157.         if test < 0xF0 then
  158.           running = test
  159.         end
  160.       end
  161.       local status = bit32.band(0xF0, event)
  162.       if status == 0x80 then -- Note off.
  163.         local channel, note, velocity = parseVoiceMessage(event)
  164.         track[cursor] = {"note_off", channel, note, velocity}
  165.       elseif status == 0x90 then -- Note on.
  166.         local channel, note, velocity = parseVoiceMessage(event)
  167.         track[cursor] = {"note_on", channel, note, velocity}
  168.       elseif status == 0xA0 then -- Aftertouch / key pressure
  169.         parseVoiceMessage(event) -- not handled
  170.       elseif status == 0xB0 then -- Controller
  171.         parseVoiceMessage(event) -- not handled
  172.       elseif status == 0xC0 then -- Program change
  173.         parseVarInt(read()) -- not handled
  174.       elseif status == 0xD0 then -- Channel pressure
  175.         parseVarInt(read()) -- not handled
  176.       elseif status == 0xE0 then -- Pitch / modulation wheel
  177.         parseVarInt(read(2), 7) -- not handled
  178.       elseif event == 0xF0 then -- System exclusive event
  179.         local length = readVariableLength()
  180.         if length > 0 then
  181.           read(length - 1)
  182.           inSysEx = read(1):byte(1) ~= 0xF7
  183.         end
  184.       elseif event == 0xF1 then -- MIDI time code quarter frame
  185.         parseVarInt(read()) -- not handled
  186.       elseif event == 0xF2 then -- Song position pointer
  187.         parseVarInt(read(2), 7) -- not handled
  188.       elseif event == 0xF3 then -- Song select
  189.         parseVarInt(read(2), 7) -- not handled
  190.       elseif event == 0xF7 then -- Divided system exclusive event
  191.         local length = readVariableLength()
  192.         if length > 0 then
  193.           read(length - 1)
  194.           inSysEx = read(1):byte(1) ~= 0xF7
  195.         else
  196.           inSysEx = false
  197.         end
  198.       elseif event >= 0xF8 and event <= 0xFE then -- System real-time event
  199.         -- not handled
  200.       elseif event == 0xFF then
  201.         -- Meta message.
  202.         local metaType = parseVarInt(read())
  203.         local length = parseVarInt(read())
  204.         local data = read(length)
  205.  
  206.         if metaType == 0x00 then -- Sequence number
  207.           track.sequence = parseVarInt(data)
  208.         elseif metaType == 0x01 then -- Text event
  209.         elseif metaType == 0x02 then -- Copyright notice
  210.         elseif metaType == 0x03 then -- Sequence / track name
  211.           track.name = data
  212.         elseif metaType == 0x04 then -- Instrument name
  213.           track.instrument = data
  214.         elseif metaType == 0x05 then -- Lyric text
  215.         elseif metaType == 0x06 then -- Marker text
  216.         elseif metaType == 0x07 then -- Cue point
  217.         elseif metaType == 0x20 then -- Channel prefix assignment
  218.         elseif metaType == 0x2F then -- End of track
  219.           track.eot = cursor
  220.         elseif metaType == 0x51 then -- Tempo setting
  221.           track[cursor] = parseVarInt(data)
  222.         elseif metaType == 0x54 then -- SMPTE offset
  223.         elseif metaType == 0x58 then -- Time signature
  224.         elseif metaType == 0x59 then -- Key signature
  225.         elseif metaType == 0x7F then -- Sequencer specific event
  226.         end
  227.       else
  228.         f:seek("cur", -9)
  229.         local area = f:read(16)
  230.         local dump = ""
  231.         for i = 1, area:len() do
  232.           dump = dump .. string.format(" %02X", area:byte(i))
  233.           if i % 4 == 0 then
  234.             dump = dump .. "\n"
  235.           end
  236.         end
  237.         error(string.format("midi file contains unhandled event types:\n0x%X at offset %d/%d\ndump of the surrounding area:\n%s", event, offset, size, dump))
  238.       end
  239.     end
  240.     local delta = size - offset
  241.     if delta ~= 0 then
  242.       f:seek("cur", delta)
  243.     end
  244.     totalOffset = totalOffset + size
  245.     table.insert(tracks, track)
  246.   else
  247.     print(string.format("Encountered unknown chunk type %s, skipping.", id))
  248.     f:seek("cur", size)
  249.   end
  250. end
  251.  
  252. f:close()
  253.  
  254. if options.i then
  255.   print(string.format("Found %d tracks, total length is %d ticks.", #tracks, totalLength))
  256.   for i, track in ipairs(tracks) do
  257.     if track.name then
  258.       print(string.format("#%d: %s", i, track.name))
  259.     end
  260.   end
  261.   return
  262. end
  263.  
  264. local removed = 0
  265. if enabledTracks.n > 0 then
  266.   for i = #tracks, 1, -1 do
  267.     if not enabledTracks[i] then
  268.       table.remove(tracks, i)
  269.       removed = removed + 1
  270.     end
  271.   end
  272. end
  273. print("Playing " .. #tracks .. " tracks:")
  274. for _, track in ipairs(tracks) do
  275.   if track.name then
  276.     print(string.format("%s", track.name))
  277.   end
  278. end
  279.  
  280. local Tbl = {format, delta, tracks, totalOffset, totalLength}
  281.  
  282. local fi = fs.open("/mnt/f34/test", "w")
  283. fi:write(text.serialize(Tbl))
  284. fi:close()
  285.  
  286. --[[local channels = {n=0}
  287. local lastTick, lastTime = 0, computer.uptime()
  288. for tick = 1, totalLength do
  289.   local hasEvent = false
  290.   for _, track in ipairs(tracks) do
  291.     if track[tick] then
  292.       hasEvent = true
  293.       break
  294.     end
  295.   end
  296.   if hasEvent then
  297.     local delay = (tick - lastTick) * time.tick() / 1000000
  298.     --local delay = delta / 20
  299.     if delay > 0.05 or computer.uptime() then
  300.       os.sleep(delay)
  301.     else
  302.       -- Busy idle otherwise, because a sleep will take up to 50ms.
  303.       local begin = os.clock()
  304.       while os.clock() - begin < delay do end
  305.     end
  306.     lastTick = tick
  307.     lastTime = computer.uptime()
  308.     for _, track in ipairs(tracks) do
  309.       local event = track[tick]
  310.       if event then
  311.         if type(event) == "number" then
  312.           time.mspb = event
  313.         elseif type(event) == "table" then
  314.           local channel, note, velocity = table.unpack(event)
  315.           local instrument
  316.           if not channels[channel] then
  317.             channels.n = channels.n + 1
  318.             channels[channel] = instruments[1 + (channels.n % #instruments)]
  319.           end
  320.           instrument = channels[channel]
  321.           -- 60 = C in MIDI, 6 = C in Minecraft
  322.           instrument.trigger((note + 6 - 60) % 24 + 1)
  323.         end
  324.       end
  325.     end
  326.   end
  327. end]]
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement