goldfiction

midi1.lua

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