Advertisement
serafim7

midiplayer [OpenComputers]

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