SHARE
TWEET

midi.lua

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