Advertisement
Ewgeniy

mid

Sep 12th, 2021
109
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 12.69 KB | None | 0 0
  1. --This program parses and plays midi files.
  2. --Files may either be played directly after parsing, or can be saved
  3. --to specialized .mdp files for direct playback on machines running midiplayer_light.lua.
  4. --This prevents the need to have fast (expensive) computers to play back music.
  5. --Should you not want to use OpenOS, this program has two stand-alone companion programs.
  6. --midiplayer_convertwrapper.lua can run on EEPROMs and will convert songs to .mdp files.
  7. --midiplayer_light can also run on EEPROMs, and will read .mdp files.
  8.  
  9. local component=require('component')
  10. local computer=require('computer')
  11. local shell=require('shell')
  12. local serialization=require('serialization')
  13. local currentTime=os.clock()
  14. local speed=1
  15.  
  16. print(string.format("Current free memory is %dk (%d%%) ",computer.freeMemory()/1024, computer.freeMemory()/computer.totalMemory()*100))
  17. local args,options=shell.parse(...)
  18. if #args>1 then speed=tonumber(args[2]) end
  19. if #args==0 or speed==nil then
  20.   print("Usage: midiplayer [-i] [-d] [-r] <filename> [speed] [track1[track2[...]]]")
  21.   print("Speed is an optional multiplier, usually needed for really simple or complex songs; default=1")
  22.   print("Track is a list of the specific tracks to play; speed multiplier must be given in this case")
  23.   print(" -i: Print track info and exit")
  24.   print(" -d: Dump track info to file (beep cards only).")
  25.   print(" -r: Read dump file instead of MIDI file.")
  26.   return
  27. end
  28.  
  29. local midiFile, errors=io.open(shell.resolve(args[1]),'rb')
  30. if not midiFile then
  31.   print(errors)
  32.   return
  33. end
  34. local fileSize=midiFile:seek('end'); midiFile:seek('set')
  35.  
  36. if options.r then --reading a beep file
  37.   local beeperEvents={}
  38.   local timeDelay = 0;
  39.   local timeLine = true
  40.   for line in midiFile:lines() do
  41.     if timeLine then
  42.       timeDelay = line
  43.       timeLine = false
  44.     else
  45.       table.insert(beeperEvents,{beeps=serialization.unserialize(line),delay=tonumber(timeDelay)})
  46.       timeLine = true
  47.     end
  48.   end
  49.   local beeper=component.getPrimary('beep')
  50.   for _,beepInfo in ipairs(beeperEvents) do
  51.     beeper.beep(beepInfo.beeps)
  52.     os.sleep(beepInfo.delay)
  53.   end
  54.   return
  55. end
  56.  
  57. --set instruments and values we need
  58. local instruments=0
  59. local playListArgs={['instrument']=false,['note']=false,['volume']=false,['frequency']=false,['duration']=false}
  60. if options.d then
  61.   print("Dumping mode selected.")
  62.   playListArgs={['frequency']=true,['duration']=true}
  63.   instruments=-1
  64. elseif component.isAvailable('iron_noteblock') then
  65.   print("Found iron noteblock")
  66.   playListArgs={['instrument']=true,['note']=true,['volume']=true}
  67.   instruments=1
  68. elseif component.isAvailable('note_block') then
  69.   print("Found note block")
  70.   playListArgs={['note']=true}
  71.   for block in computer.list('note_block') do
  72.     instrumnets=instruments+1
  73.     instrument[call..instrument.n]=component.proxy(block)
  74.   end
  75. elseif component.isAvailable('beep') then
  76.   print("Found beep card")
  77.   playListArgs={['frequency']=true,['duration']=true}
  78.   instruments=-1
  79. else
  80.   print("No sound items found, defaulting to PC speaker in single-track mode")
  81.   playListArgs={['frequency']=true,['duration']=true}
  82. end
  83.  
  84. --helper functions  
  85. local function hexToDec(bytes)
  86.   local total=0
  87.   for i=1, bytes:len() do
  88.     total=bit32.lshift(total,8)+bytes:byte(i)
  89.   end
  90.   return total
  91. end
  92.  
  93. local fileHeader=midiFile:read(4)
  94. local headerSize=hexToDec(midiFile:read(4))
  95. local fileFormat=hexToDec(midiFile:read(2))
  96. local numTracks=hexToDec(midiFile:read(2))
  97. local timeDivision=hexToDec(midiFile:read(2))
  98. if fileHeader ~= 'MThd' or headerSize ~= 6 then
  99.   print("Error in parsing header data.  File is likely corrupt")
  100.   return
  101. elseif fileFormat < 0 or fileFormat > 2 then
  102.   print("Unsupported file format.  MIDI may be corrupt")
  103.   return
  104. elseif fileFormat==2 then
  105.   print("Asynchronous file format not suppported")
  106.   return
  107. elseif fileFormat==1 then
  108.   print(string.format("Synchronous file format found with %d tracks.", numTracks))
  109. else
  110.   print("Single track found.")
  111. end
  112.  
  113. local tickLength=0
  114. local spb=0.5
  115. local tpb=0
  116. if bit32.rshift(timeDivision,15)==0 then
  117.   tpb=bit32.band(timeDivision,0x7FFF)
  118.   tickLength=(spb / tpb)
  119. else
  120.   local fps=math.floor(bit32.extract(timeDivision,1,7))
  121.   local tpf=bit32.rshift(timeDivision,8)
  122.   tickLength=1/(tpf*fps); spb=nil
  123. end
  124.  
  125. --Get track offsets
  126. local tracks={}
  127. for i=1,numTracks do
  128.   local trackInfo={instrument='Unknown',instrumentID=0,ID=i}
  129.   if midiFile:read(4)~="MTrk" then
  130.     print("Invalid track found, attempting to skip")
  131.     midiFile:seek('cur', hexToDec(midiFile:read(4)))
  132.   else
  133.     trackInfo.size=hexToDec(midiFile:read(4))
  134.     trackInfo.offset=midiFile:seek()
  135.   end
  136.   if #args<=2 then
  137.     trackInfo.play=true
  138.   else
  139.     if instruments==0 and i==tostring(args[3]) then
  140.       trackInfo.play=true
  141.     else
  142.       for _,v in pairs(args) do
  143.         if tostring(i)==v then trackInfo.play=true break end
  144.       end
  145.     end
  146.   end
  147.   tracks[i]=trackInfo
  148.   midiFile:seek('set',trackInfo.offset+trackInfo.size)
  149. end
  150. midiFile:seek('set',tracks[1].offset)
  151.  
  152.  
  153. --Parse ALL the things (that we need)
  154. local fireTicks={}
  155. for i=1,numTracks do
  156.   local onNotes={}
  157.   local currentTick=0
  158.   local moreData=true
  159.   local previousEventType=''
  160.   local constPassEvent={[0xA]=2,[0xB]=2,[0xD]=1,[0xE]=2,[0xF1]=1,[0xF2]=2,[0xF3]=1,[0xF6]=0,[0xF8]=0,[0xFA]=0,[0xFB]=0,[0xFC]=0,[0xFE]=0,[0x00]=2,[0x20]=2,[0x21]=2,[0x54]=6,[0x59]=3}
  161.   local varPassEvent={[0x01]=true,[0x05]=true,[0x06]=true,[0x07]=true,[0x7F]=true}
  162.    
  163.   local function calculateDuration(midiFile,tickLength,fireTicks,onNotes,eventID)
  164.     --Issue with notes occurs if there's multiple on events in a row for the same note.
  165.     --Fixed for now, but it shortens the duration a bit.
  166.     if not onNotes[eventID] then
  167.       print('Off note with no corresponding on note found at byte:',midiFile:seek())
  168.     elseif not fireTicks[onNotes[eventID]] then
  169.       print('Off note with no corresponding tick found at byte:',midiFile:seek())
  170.     else
  171.       for _,firingNotes in pairs(fireTicks[onNotes[eventID]]) do
  172.         if firingNotes.duration==eventID then
  173.           firingNotes.duration=(currentTick-onNotes[eventID])*tickLength
  174.           onNotes[eventID]=nil
  175.           return
  176.         end
  177.       end
  178.     end
  179.   end
  180.  
  181.   if tracks[i].play then
  182.     while moreData do
  183.       local test
  184.       local eventType
  185.       local eventTime=0
  186.      
  187.       repeat
  188.         test=midiFile:read(1):byte()
  189.         eventTime=bit32.lshift(eventTime,7)+bit32.extract(test,0,7)
  190.       until bit32.extract(test,7)==0
  191.    
  192.       currentTick=currentTick+eventTime
  193.       eventType=midiFile:read(1)
  194.       if bit32.extract(eventType:byte(),7)==0 then
  195.         eventType=previousEventType
  196.         midiFile:seek('cur',-1)
  197.       else
  198.         eventType=eventType:byte()
  199.       end
  200.       if bit32.rshift(eventType,4)==8 then --Note off
  201.         if playListArgs.duration then
  202.           calculateDuration(midiFile,tickLength,fireTicks,onNotes,bit32.extract(eventType,0,4)..(2^((midiFile:read(1):byte()-69)/12)*440))
  203.           midiFile:seek('cur',1)
  204.         else
  205.           midiFile:seek('cur',2)
  206.         end
  207.       elseif bit32.rshift(eventType,4)==9 then --Note on
  208.         local noteInfo={}
  209.         local note=midiFile:read(1):byte()
  210.         local volume=midiFile:read(1):byte()/127
  211.         local frequency=(2^((note - 69) / 12) * 440)
  212.         if volume==0 then --Really a note off command
  213.           if playListArgs.duration then
  214.             calculateDuration(midiFile,tickLength,fireTicks,onNotes,bit32.extract(eventType,0,4)..frequency)
  215.           end
  216.         else
  217.           if playListArgs.note then noteInfo.note=((note-60+6)%24+1) end
  218.           if playListArgs.volume then noteInfo.volume=volume end
  219.           if playListArgs.frequency then --Implies duration
  220.             noteInfo.frequency=(2^((note - 69) / 12) * 440)
  221.             noteInfo.duration=bit32.extract(eventType,0,4)..noteInfo.frequency
  222.             onNotes[bit32.extract(eventType,0,4)..noteInfo.frequency]=currentTick
  223.           end
  224.           if not fireTicks[currentTick] then fireTicks[currentTick]={} end
  225.           table.insert(fireTicks[currentTick],noteInfo)
  226.         end
  227.       elseif bit32.rshift(eventType,4)==0xC then --Instrument setting
  228.         test=midiFile:read(1):byte()
  229.         if bit32.lshift(eventType,4)==0x9 then
  230.           tracks[i].instrumentID=2
  231.         elseif test>=0x18 and test<0x38 then
  232.           tracks[i].instrumentID=4
  233.         else
  234.           tracks[i].instrumentID=5
  235.         end
  236.       elseif eventType==0xF0 then  --Sysex message (variable length)
  237.         repeat test=string.byte(midiFile:read(1)) until bit32.extract(test,7)~='1'
  238.       elseif eventType==0xFF then --Meta message
  239.         local metaType=midiFile:read(1):byte()
  240.         if metaType==0x02 then --Copyright notice
  241.           print(midiFile:read(midiFile:read(1):byte()))
  242.         elseif metaType==0x03 then --Track name
  243.           tracks[i].name=midiFile:read(midiFile:read(1):byte())
  244.         elseif metaType==0x04 then --Instrument name
  245.           tracks[i].instrument=midiFile:read(midiFile:read(1):byte())
  246.         elseif metaType==0x2F then--EOT
  247.           midiFile:seek('cur',9); moreData=false
  248.         elseif metaType==0x51 then --Set tempo
  249.           spb=hexToDec(midiFile:read(midiFile:read(1):byte()))/1000000
  250.           tickLength=spb/tpb
  251.           print(string.format("Tick length set to %f seconds by metadata in file", tickLength))
  252.         elseif metaType==0x58 then --Time signature
  253.           midiFile:seek('cur',1)
  254.           local num=midiFile:read(1):byte()
  255.           local den=2^midiFile:read(1):byte()
  256.           tracks[i].timesignature=tostring(num) .. '/' .. tostring(den)
  257.           midiFile:seek('cur',2)
  258.         elseif varPassEvent[metaType] then
  259.           midiFile:seek('cur',midiFile:read(1):byte())
  260.         elseif constPassEvent[metaType] then
  261.           midiFile:seek('cur',constPassEvent[metaType])
  262.         else
  263.           print(string.format("Unknown meta event type %02X encountered at byte %d.", eventType, midiFile:seek()))
  264.         end
  265.       elseif constPassEvent[bit32.rshift(eventType,4)] then
  266.         midiFile:seek('cur',constPassEvent[bit32.rshift(eventType,4)])
  267.       elseif constPassEvent[eventType] then
  268.         midiFile:seek('cur',constPassEvent[eventType])
  269.       else
  270.           print(string.format("Unknown regular event type %02X encountered at byte %d.", eventType, midiFile:seek()))
  271.       end
  272.       previousEventType=eventType
  273.     end
  274.   else
  275.     midiFile:seek('cur',tracks[i].size+8)
  276.   end
  277. end
  278. midiFile:close()
  279.  
  280. print("Track","Name","Instrument")
  281. for i=1,numTracks do
  282.   if tracks[i].play then print(tracks[i].ID,tracks[i].name,tracks[i].Instrument) end
  283. end
  284. print('Notes ready in', os.clock()-currentTime)
  285. print(string.format("Current free memory is %dk (%d%%) ",computer.freeMemory()/1024, computer.freeMemory()/computer.totalMemory()*100))
  286. if options.i then return end
  287. if not options.d then
  288.   print('Press any key to play.')
  289.   io.read()
  290. end
  291.  
  292. local fireEvents={}
  293. local numEvents=0
  294. for key,_ in pairs(fireTicks) do
  295.   table.insert(fireEvents,key)
  296.   numEvents=numEvents+1
  297. end
  298. table.sort(fireEvents)
  299. fireEvents[numEvents+1]=fireEvents[numEvents]
  300.  
  301. if instruments==-1 or options.d then
  302.   local beeperEvents={}
  303.   for i=1,numEvents do
  304.     local beeps={}
  305.     for _,noteInfo in pairs(fireTicks[fireEvents[i]]) do
  306.       if tonumber(noteInfo.duration) < 200 then
  307.         beeps[math.max(math.min(noteInfo.frequency,2000),20)]=tonumber(noteInfo.duration)
  308.       end
  309.     end
  310.     table.insert(beeperEvents,{beeps=beeps,delay=(fireEvents[i+1]-fireEvents[i])*tickLength-0.081*speed}) --0.081 is an emperical constant
  311.     fireTicks[fireEvents[i]]=nil
  312.   end
  313.   if options.d then
  314.     local dumpFile = io.open(shell.resolve(args[1]):sub(1,shell.resolve(args[1]):len()-4) .. ".mdp",'w')
  315.     for _,beepInfo in ipairs(beeperEvents) do
  316.       dumpFile:write(tostring(beepInfo.delay) .. "\n")
  317.       dumpFile:write(serialization.serialize(beepInfo.beeps)  .. "\n")
  318.     end
  319.     dumpFile:flush()
  320.     dumpFile:close()
  321.     return
  322.   else
  323.     local beeper=component.getPrimary('beep')
  324.     for _,beepInfo in ipairs(beeperEvents) do
  325.       beeper.beep(beepInfo.beeps)
  326.       os.sleep(beepInfo.delay)
  327.     end
  328.   end
  329.  
  330. elseif instruments==1 then
  331.   local instrument=component.getPrimary('iron_noteblock')
  332.   for i=1,numEvents do
  333.     for _,noteInfo in pairs(fireTicks[fireEvents[i]]) do
  334.       instrument.playNote(noteInfo.instrument,noteInfo.note,noteInfo.volume)
  335.     end
  336.     os.sleep((fireEvents[i+1]-fireEvents[i])*tickLength-0.05)
  337.   end
  338.  
  339. elseif instruments==0 then
  340.   for i=1,numEvents do
  341.     computer.beep(fireTicks[fireEvents[i]][1].frequency,fireTicks[fireEvents[i]][1].duration)
  342.     os.sleep((fireEvents[i+1]-fireEvents[i])*tickLength-fireTicks[fireEvents[i]][1].duration-0.05)
  343.   end
  344. end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement