Advertisement
Schneemann

test1

May 6th, 2021
91
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 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