Advertisement
Guest User

Untitled

a guest
Jul 23rd, 2019
116
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 26.56 KB | None | 0 0
  1. --[[
  2. wave-amp version 1.0.0
  3.  
  4. The MIT License (MIT)
  5. Copyright (c) 2016 CrazedProgrammer
  6.  
  7. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
  8. associated documentation files (the "Software"), to deal in the Software without restriction,
  9. including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
  10. and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do
  11. so, subject to the following conditions:
  12.  
  13. The above copyright notice and this permission notice shall be included in all copies or
  14. substantial portions of the Software.
  15.  
  16. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
  17. INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
  18. PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
  19. COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
  20. AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  21. WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  22. ]]
  23.  
  24. --[[
  25. wave version 0.1.4
  26.  
  27. The MIT License (MIT)
  28. Copyright (c) 2016 CrazedProgrammer
  29.  
  30. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
  31. associated documentation files (the "Software"), to deal in the Software without restriction,
  32. including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
  33. and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do
  34. so, subject to the following conditions:
  35.  
  36. The above copyright notice and this permission notice shall be included in all copies or
  37. substantial portions of the Software.
  38.  
  39. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
  40. INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
  41. PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
  42. COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
  43. AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  44. WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  45. ]]
  46.  
  47. local wave = { }
  48. wave.version = "0.1.4"
  49.  
  50. wave._oldSoundMap = {"harp", "bassattack", "bd", "snare", "hat"}
  51. wave._newSoundMap = {"harp", "bass", "basedrum", "snare", "hat"}
  52. wave._defaultThrottle = 99
  53. wave._defaultClipMode = 1
  54. wave._maxInterval = 1
  55. wave._isNewSystem = false
  56. if _HOST then
  57. wave._isNewSystem = _HOST:sub(15, #_HOST) >= "1.80"
  58. end
  59.  
  60. wave.context = { }
  61. wave.output = { }
  62. wave.track = { }
  63. wave.instance = { }
  64.  
  65. function wave.createContext(clock, volume)
  66. clock = clock or os.clock()
  67. volume = volume or 1.0
  68.  
  69. local context = setmetatable({ }, {__index = wave.context})
  70. context.outputs = { }
  71. context.instances = { }
  72. context.vs = {0, 0, 0, 0, 0}
  73. context.prevClock = clock
  74. context.volume = volume
  75. return context
  76. end
  77.  
  78. function wave.context:addOutput(...)
  79. local output = wave.createOutput(...)
  80. self.outputs[#self.outputs + 1] = output
  81. return output
  82. end
  83.  
  84. function wave.context:addOutputs(...)
  85. local outs = {...}
  86. if #outs == 1 then
  87. if not getmetatable(outs) then
  88. outs = outs[1]
  89. else
  90. if getmetatable(outs).__index ~= wave.outputs then
  91. outs = outs[1]
  92. end
  93. end
  94. end
  95. for i = 1, #outs do
  96. self:addOutput(outs[i])
  97. end
  98. end
  99.  
  100. function wave.context:removeOutput(out)
  101. if type(out) == "number" then
  102. table.remove(self.outputs, out)
  103. return
  104. elseif type(out) == "table" then
  105. if getmetatable(out).__index == wave.output then
  106. for i = 1, #self.outputs do
  107. if out == self.outputs[i] then
  108. table.remove(self.outputs, i)
  109. return
  110. end
  111. end
  112. return
  113. end
  114. end
  115. for i = 1, #self.outputs do
  116. if out == self.outputs[i].native then
  117. table.remove(self.outputs, i)
  118. return
  119. end
  120. end
  121. end
  122.  
  123. function wave.context:addInstance(...)
  124. local instance = wave.createInstance(...)
  125. self.instances[#self.instances + 1] = instance
  126. return instance
  127. end
  128.  
  129. function wave.context:removeInstance(instance)
  130. if type(instance) == "number" then
  131. table.remove(self.instances, instance)
  132. else
  133. for i = 1, #self.instances do
  134. if self.instances == instance then
  135. table.remove(self.instances, i)
  136. return
  137. end
  138. end
  139. end
  140. end
  141.  
  142. function wave.context:playNote(note, pitch, volume)
  143. volume = volume or 1.0
  144.  
  145. self.vs[note] = self.vs[note] + volume
  146. for i = 1, #self.outputs do
  147. self.outputs[i]:playNote(note, pitch, volume * self.volume)
  148. end
  149. end
  150.  
  151. function wave.context:update(interval)
  152. local clock = os.clock()
  153. interval = interval or (clock - self.prevClock)
  154.  
  155. self.prevClock = clock
  156. if interval > wave._maxInterval then
  157. interval = wave._maxInterval
  158. end
  159. for i = 1, #self.outputs do
  160. self.outputs[i].notes = 0
  161. end
  162. for i = 1, 5 do
  163. self.vs[i] = 0
  164. end
  165. if interval > 0 then
  166. for i = 1, #self.instances do
  167. local notes = self.instances[i]:update(interval)
  168. for j = 1, #notes / 3 do
  169. self:playNote(notes[j * 3 - 2], notes[j * 3 - 1], notes[j * 3])
  170. end
  171. end
  172. end
  173. end
  174.  
  175.  
  176.  
  177. function wave.createOutput(out, volume, filter, throttle, clipMode)
  178. volume = volume or 1.0
  179. filter = filter or {true, true, true, true, true}
  180. throttle = throttle or wave._defaultThrottle
  181. clipMode = clipMode or wave._defaultClipMode
  182.  
  183. local output = setmetatable({ }, {__index = wave.output})
  184. output.native = out
  185. output.volume = volume
  186. output.filter = filter
  187. output.notes = 0
  188. output.throttle = throttle
  189. output.clipMode = clipMode
  190. if type(out) == "function" then
  191. output.nativePlayNote = out
  192. output.type = "custom"
  193. return output
  194. elseif type(out) == "string" then
  195. if peripheral.getType(out) == "speaker" then
  196. if wave._isNewSystem then
  197. local nb = peripheral.wrap(out)
  198. output.type = "iron_noteblock"
  199. function output.nativePlayNote(note, pitch, volume)
  200. if output.volume * volume > 0 then
  201. nb.playSound("minecraft:block.note."..wave._newSoundMap[note], volume, math.pow(2, (pitch - 12) / 12))
  202. end
  203. end
  204. return output
  205. end
  206. end
  207. elseif type(out) == "table" then
  208. if out.execAsync then
  209. output.type = "commands"
  210. if wave._isNewSystem then
  211. function output.nativePlayNote(note, pitch, volume)
  212. out.execAsync("playsound minecraft:block.note."..wave._newSoundMap[note].." record @a ~ ~ ~ "..tostring(volume).." "..tostring(math.pow(2, (pitch - 12) / 12)))
  213. end
  214. else
  215. function output.nativePlayNote(note, pitch, volume)
  216. out.execAsync("playsound note."..wave._oldSoundMap[note].." @a ~ ~ ~ "..tostring(volume).." "..tostring(math.pow(2, (pitch - 12) / 12)))
  217. end
  218. end
  219. return output
  220. elseif getmetatable(out) then
  221. if getmetatable(out).__index == wave.output then
  222. return out
  223. end
  224. end
  225. end
  226. end
  227.  
  228. function wave.scanOutputs()
  229. local outs = { }
  230. if commands then
  231. outs[#outs + 1] = wave.createOutput(commands)
  232. end
  233. local sides = peripheral.getNames()
  234. for i = 1, #sides do
  235. if peripheral.getType(sides[i]) == "iron_noteblock" then
  236. outs[#outs + 1] = wave.createOutput(sides[i])
  237. end
  238. end
  239. return outs
  240. end
  241.  
  242. function wave.output:playNote(note, pitch, volume)
  243. volume = volume or 1.0
  244.  
  245. if self.clipMode == 1 then
  246. if pitch < 0 then
  247. pitch = 0
  248. elseif pitch > 24 then
  249. pitch = 24
  250. end
  251. elseif self.clipMode == 2 then
  252. if pitch < 0 then
  253. while pitch < 0 do
  254. pitch = pitch + 12
  255. end
  256. elseif pitch > 24 then
  257. while pitch > 24 do
  258. pitch = pitch - 12
  259. end
  260. end
  261. end
  262. if self.filter[note] and self.notes < self.throttle then
  263. self.nativePlayNote(note, pitch, volume * self.volume)
  264. self.notes = self.notes + 1
  265. end
  266. end
  267.  
  268.  
  269.  
  270. function wave.loadTrack(path)
  271. local track = setmetatable({ }, {__index = wave.track})
  272. local handle = fs.open(path, "rb")
  273. if not handle then return end
  274.  
  275. local function readInt(size)
  276. local num = 0
  277. for i = 0, size - 1 do
  278. local byte = handle.read()
  279. if not byte then -- dont leave open file handles no matter what
  280. handle.close()
  281. return
  282. end
  283. num = num + byte * (256 ^ i)
  284. end
  285. return num
  286. end
  287. local function readStr()
  288. local length = readInt(4)
  289. if not length then return end
  290. local data = { }
  291. for i = 1, length do
  292. data[i] = string.char(handle.read())
  293. end
  294. return table.concat(data)
  295. end
  296.  
  297. -- Part #1: Metadata
  298. track.length = readInt(2) -- song length (ticks)
  299. track.height = readInt(2) -- song height
  300. track.name = readStr() -- song name
  301. track.author = readStr() -- song author
  302. track.originalAuthor = readStr() -- original song author
  303. track.description = readStr() -- song description
  304. track.tempo = readInt(2) / 100 -- tempo (ticks per second)
  305. track.autoSaving = readInt(1) == 0 and true or false -- auto-saving
  306. track.autoSavingDuration = readInt(1) -- auto-saving duration
  307. track.timeSignature = readInt(1) -- time signature (3 = 3/4)
  308. track.minutesSpent = readInt(4) -- minutes spent
  309. track.leftClicks = readInt(4) -- left clicks
  310. track.rightClicks = readInt(4) -- right clicks
  311. track.blocksAdded = readInt(4) -- blocks added
  312. track.blocksRemoved = readInt(4) -- blocks removed
  313. track.schematicFileName = readStr() -- midi/schematic file name
  314.  
  315. -- Part #2: Notes
  316. track.layers = { }
  317. for i = 1, track.height do
  318. track.layers[i] = {name = "Layer "..i, volume = 1.0}
  319. track.layers[i].notes = { }
  320. end
  321.  
  322. local tick = 0
  323. while true do
  324. local tickJumps = readInt(2)
  325. if tickJumps == 0 then break end
  326. tick = tick + tickJumps
  327. local layer = 0
  328. while true do
  329. local layerJumps = readInt(2)
  330. if layerJumps == 0 then break end
  331. layer = layer + layerJumps
  332. if layer > track.height then -- nbs can be buggy
  333. for i = track.height + 1, layer do
  334. track.layers[i] = {name = "Layer "..i, volume = 1.0}
  335. track.layers[i].notes = { }
  336. end
  337. track.height = layer
  338. end
  339. local instrument = readInt(1)
  340. local key = readInt(1)
  341. if instrument <= 4 then -- nbs can be buggy
  342. track.layers[layer].notes[tick * 2 - 1] = instrument + 1
  343. track.layers[layer].notes[tick * 2] = key - 33
  344. end
  345. end
  346. end
  347.  
  348. -- Part #3: Layers
  349. for i = 1, track.height do
  350. local name = readStr()
  351. if not name then break end -- if layer data doesnt exist, abort
  352. track.layers[i].name = name
  353. track.layers[i].volume = readInt(1) / 100
  354. end
  355.  
  356. handle.close()
  357. return track
  358. end
  359.  
  360.  
  361.  
  362. function wave.createInstance(track, volume, playing, loop)
  363. volume = volume or 1.0
  364. playing = (playing == nil) or playing
  365. loop = (loop ~= nil) and loop
  366.  
  367. if getmetatable(track).__index == wave.instance then
  368. return track
  369. end
  370. local instance = setmetatable({ }, {__index = wave.instance})
  371. instance.track = track
  372. instance.volume = volume or 1.0
  373. instance.playing = playing
  374. instance.loop = loop
  375. instance.tick = 1
  376. return instance
  377. end
  378.  
  379. function wave.instance:update(interval)
  380. local notes = { }
  381. if self.playing then
  382. local dticks = interval * self.track.tempo
  383. local starttick = self.tick
  384. local endtick = starttick + dticks
  385. local istarttick = math.ceil(starttick)
  386. local iendtick = math.ceil(endtick) - 1
  387. for i = istarttick, iendtick do
  388. for j = 1, self.track.height do
  389. if self.track.layers[j].notes[i * 2 - 1] then
  390. notes[#notes + 1] = self.track.layers[j].notes[i * 2 - 1]
  391. notes[#notes + 1] = self.track.layers[j].notes[i * 2]
  392. notes[#notes + 1] = self.track.layers[j].volume
  393. end
  394. end
  395. end
  396. self.tick = self.tick + dticks
  397.  
  398. if endtick > self.track.length then
  399. self.tick = 1
  400. self.playing = self.loop
  401. end
  402. end
  403. return notes
  404. end
  405.  
  406.  
  407.  
  408. local cmdHelp = [[
  409. -l lists all outputs connected to the computer.
  410. -c <config file> loads the parameters from a file.
  411. parameters are separated by newlines.
  412. -t <theme file> loads the theme from a file.
  413. -f <filter[:second]> sets the note filter for the outputs.
  414. examples:
  415. -f 10111 sets the filter for all outputs to remove the bass instrument.
  416. -f 10011:01100 sets the filter so the bass and basedrum instruments only come out of the second output
  417. -v <volume[:second]> sets the volume for the outputs.
  418. --nrm --stp --rep --shf sets the play mode.
  419. --noui --noinput disables the ui/keyboard input]]
  420.  
  421.  
  422. local trackMode = 1
  423. -- 1 = normal (go to next song on finish)
  424. -- 2 = stop (stop on finish)
  425. -- 3 = repeat (restart song on finish)
  426. -- 4 = shuffle (go to random song on finish)
  427.  
  428. local files = { }
  429. local tracks = { }
  430. local context, track, instance
  431.  
  432. -- ui stuff
  433. local noUI = false
  434. local noInput = false
  435. local screenWidth, screenHeight = term.getSize()
  436. local trackScroll = 0
  437. local currentTrack = 1
  438. local vsEasings = {0, 0, 0, 0, 0}
  439. local vsStep = 5
  440. local vsDecline = 0.25
  441.  
  442. -- theme
  443. local theme = term.isColor() and
  444. {
  445. topBar = colors.lime,
  446. topBarTitle = colors.white,
  447. topBarOption = colors.white,
  448. topBarOptionSelected = colors.lightGray,
  449. topBarClose = colors.white,
  450. song = colors.black,
  451. songBackground = colors.white,
  452. songSelected = colors.black,
  453. songSelectedBackground = colors.lightGray,
  454. scrollBackground = colors.lightGray,
  455. scrollBar = colors.gray,
  456. scrollButton = colors.black,
  457. visualiserBar = colors.lime,
  458. visualiserBackground = colors.green,
  459. progressTime = colors.white,
  460. progressBackground = colors.lightGray,
  461. progressLine = colors.gray,
  462. progressNub = colors.gray,
  463. progressNubBackground = colors.gray,
  464. progressNubChar = "=",
  465. progressButton = colors.white
  466. }
  467. or
  468. {
  469. topBar = colors.lightGray,
  470. topBarTitle = colors.white,
  471. topBarOption = colors.white,
  472. topBarOptionSelected = colors.gray,
  473. topBarClose = colors.white,
  474. song = colors.black,
  475. songBackground = colors.white,
  476. songSelected = colors.black,
  477. songSelectedBackground = colors.lightGray,
  478. scrollBackground = colors.lightGray,
  479. scrollBar = colors.gray,
  480. scrollButton = colors.black,
  481. visualiserBar = colors.black,
  482. visualiserBackground = colors.gray,
  483. progressTime = colors.white,
  484. progressBackground = colors.lightGray,
  485. progressLine = colors.gray,
  486. progressNub = colors.gray,
  487. progressNubBackground = colors.gray,
  488. progressNubChar = "=",
  489. progressButton = colors.white
  490. }
  491.  
  492. local running = true
  493.  
  494.  
  495.  
  496. local function addFiles(path)
  497. local dirstack = {path}
  498. while #dirstack > 0 do
  499. local dir = dirstack[1]
  500. table.remove(dirstack, 1)
  501. if dir ~= "rom" then
  502. for _, v in pairs(fs.list(dir)) do
  503. local path = (dir == "") and v or dir.."/"..v
  504. if fs.isDir(path) then
  505. dirstack[#dirstack + 1] = path
  506. elseif path:sub(#path - 3, #path) == ".nbs" then
  507. files[#files + 1] = path
  508. end
  509. end
  510. end
  511. end
  512. end
  513.  
  514. local function init(args)
  515. local volumes = { }
  516. local filters = { }
  517. local outputs = wave.scanOutputs()
  518. local timestamp = 0
  519.  
  520. if #outputs == 0 then
  521. error("no outputs found")
  522. end
  523.  
  524. local i, argtype = 1
  525. while i <= #args do
  526. if not argtype then
  527. if args[i] == "-h" then
  528. print(cmdHelp)
  529. noUI = true
  530. running = false
  531. return
  532. elseif args[i] == "-c" or args[i] == "-v" or args[i] == "-f" or args[i] == "-t" then
  533. argtype = args[i]
  534. elseif args[i] == "-l" then
  535. print(#outputs.." outputs detected:")
  536. for i = 1, #outputs do
  537. print(i..":", outputs[i].type, type(outputs[i].native) == "string" and outputs[i].native or "")
  538. end
  539. noUI = true
  540. running = false
  541. return
  542. elseif args[i] == "--noui" then
  543. noUI = true
  544. elseif args[i] == "--noinput" then
  545. noInput = true
  546. elseif args[i] == "--nrm" then
  547. trackMode = 1
  548. elseif args[i] == "--stp" then
  549. trackMode = 2
  550. elseif args[i] == "--rep" then
  551. trackMode = 3
  552. elseif args[i] == "--shf" then
  553. trackMode = 4
  554. else
  555. local path = shell.resolve(args[i])
  556. if fs.isDir(path) then
  557. addFiles(path)
  558. elseif fs.exists(path) then
  559. files[#files + 1] = path
  560. end
  561. end
  562. else
  563. if argtype == "-c" then
  564. local path = shell.resolve(args[i])
  565. local handle = fs.open(path, "r")
  566. if not handle then
  567. error("config file does not exist: "..path)
  568. end
  569. local line = handle.readLine()
  570. while line do
  571. args[#args + 1] = line
  572. line = handle.readLine()
  573. end
  574. handle.close()
  575. elseif argtype == "-t" then
  576. local path = shell.resolve(args[i])
  577. local handle = fs.open(path, "r")
  578. if not handle then
  579. error("theme file does not exist: "..path)
  580. end
  581. local data = handle.readAll()
  582. handle.close()
  583. for k, v in pairs(colors) do
  584. data = data:gsub("colors."..k, tostring(v))
  585. end
  586. for k, v in pairs(colours) do
  587. data = data:gsub("colours."..k, tostring(v))
  588. end
  589. local newtheme = textutils.unserialize(data)
  590. for k, v in pairs(newtheme) do
  591. theme[k] = v
  592. end
  593. elseif argtype == "-v" then
  594. for str in args[i]:gmatch("([^:]+)") do
  595. local vol = tonumber(str)
  596. if vol then
  597. if vol >= 0 and vol <= 1 then
  598. volumes[#volumes + 1] = vol
  599. else
  600. error("invalid volume value: "..str)
  601. end
  602. else
  603. error("invalid volume value: "..str)
  604. end
  605. end
  606. elseif argtype == "-f" then
  607. for str in args[i]:gmatch("([^:]+)") do
  608. if #str == 5 then
  609. local filter = { }
  610. for i = 1, 5 do
  611. if str:sub(i, i) == "1" then
  612. filter[i] = true
  613. elseif str:sub(i, i) == "0" then
  614. filter[i] = false
  615. else
  616. error("invalid filter value: "..str)
  617. end
  618. end
  619. filters[#filters + 1] = filter
  620. else
  621. error("invalid filter value: "..str)
  622. end
  623. end
  624. end
  625. argtype = nil
  626. end
  627. i = i + 1
  628. end
  629.  
  630. if #files == 0 then
  631. addFiles("")
  632. end
  633.  
  634. i = 1
  635. print("loading tracks...")
  636. while i <= #files do
  637. local track
  638. pcall(function () track = wave.loadTrack(files[i]) end)
  639. if not track then
  640. print("failed to load "..files[i])
  641. os.sleep(0.2)
  642. table.remove(files, i)
  643. else
  644. tracks[i] = track
  645. print("loaded "..files[i])
  646. i = i + 1
  647. end
  648. if i % 10 == 0 then
  649. os.sleep(0)
  650. end
  651. end
  652. if #files == 0 then
  653. error("no tracks found")
  654. end
  655.  
  656. if #volumes == 0 then
  657. volumes[1] = 1
  658. end
  659. if #filters == 0 then
  660. filters[1] = {true, true, true, true, true}
  661. end
  662. if #volumes == 1 then
  663. for i = 2, #outputs do
  664. volumes[i] = volumes[1]
  665. end
  666. end
  667. if #filters == 1 then
  668. for i = 2, #outputs do
  669. filters[i] = filters[1]
  670. end
  671. end
  672. if #volumes ~= #outputs then
  673. error("invalid amount of volume values: "..#volumes.." (must be 1 or "..#outputs..")")
  674. end
  675. if #filters ~= #outputs then
  676. error("invalid amount of filter values: "..#filters.." (must be 1 or "..#outputs..")")
  677. end
  678.  
  679. for i = 1, #outputs do
  680. outputs[i].volume = volumes[i]
  681. outputs[i].filter = filters[i]
  682. end
  683.  
  684. context = wave.createContext()
  685. context:addOutputs(outputs)
  686. end
  687.  
  688.  
  689.  
  690.  
  691. local function formatTime(secs)
  692. local mins = math.floor(secs / 60)
  693. secs = secs - mins * 60
  694. return string.format("%01d:%02d", mins, secs)
  695. end
  696.  
  697. local function drawStatic()
  698. if noUI then return end
  699. term.setCursorPos(1, 1)
  700. term.setBackgroundColor(theme.topBar)
  701. term.setTextColor(theme.topBarTitle)
  702. term.write("wave-amp")
  703. term.write((" "):rep(screenWidth - 25))
  704. term.setTextColor(trackMode == 1 and theme.topBarOptionSelected or theme.topBarOption)
  705. term.write("nrm ")
  706. term.setTextColor(trackMode == 2 and theme.topBarOptionSelected or theme.topBarOption)
  707. term.write("stp ")
  708. term.setTextColor(trackMode == 3 and theme.topBarOptionSelected or theme.topBarOption)
  709. term.write("rep ")
  710. term.setTextColor(trackMode == 4 and theme.topBarOptionSelected or theme.topBarOption)
  711. term.write("shf ")
  712. term.setTextColor(theme.topBarClose)
  713. term.write("X")
  714.  
  715. local scrollnub = math.floor(trackScroll / (#tracks - screenHeight + 7) * (screenHeight - 10) + 0.5)
  716.  
  717. term.setTextColor(theme.song)
  718. term.setBackgroundColor(theme.songBackground)
  719. for i = 1, screenHeight - 7 do
  720. local index = i + trackScroll
  721. term.setCursorPos(1, i + 1)
  722. term.setTextColor(index == currentTrack and theme.songSelected or theme.song)
  723. term.setBackgroundColor(index == currentTrack and theme.songSelectedBackground or theme.songBackground)
  724. local str = ""
  725. if tracks[index] then
  726. local track = tracks[index]
  727. str = formatTime(track.length / track.tempo).." "
  728. if #track.name > 0 then
  729. str = str..(#track.originalAuthor == 0 and track.author or track.originalAuthor).." - "..track.name
  730. else
  731. local name = fs.getName(files[index])
  732. str = str..name:sub(1, #name - 4)
  733. end
  734. end
  735. if #str > screenWidth - 1 then
  736. str = str:sub(1, screenWidth - 3)..".."
  737. end
  738. term.write(str)
  739. term.write((" "):rep(screenWidth - 1 - #str))
  740. term.setBackgroundColor((i >= scrollnub + 1 and i <= scrollnub + 3) and theme.scrollBar or theme.scrollBackground)
  741. if i == 1 then
  742. term.setTextColor(theme.scrollButton)
  743. term.write(_HOST and "\30" or "^")
  744. elseif i == screenHeight - 7 then
  745. term.setTextColor(theme.scrollButton)
  746. term.write(_HOST and "\31" or "v")
  747. else
  748. term.write(" ")
  749. end
  750. end
  751. end
  752.  
  753. local function drawDynamic()
  754. if noUI then return end
  755. for i = 1, 5 do
  756. vsEasings[i] = vsEasings[i] - vsDecline
  757. if vsEasings[i] < 0 then
  758. vsEasings[i] = 0
  759. end
  760. local part = context.vs[i] > vsStep and vsStep or context.vs[i]
  761. if vsEasings[i] < part then
  762. vsEasings[i] = part
  763. end
  764. local full = math.floor(part / vsStep * screenWidth + 0.5)
  765. local easing = math.floor(vsEasings[i] / vsStep * screenWidth + 0.5)
  766. term.setCursorPos(1, screenHeight - 6 + i)
  767. term.setBackgroundColor(theme.visualiserBar)
  768. term.setTextColor(theme.visualiserBackground)
  769. term.write((" "):rep(full))
  770. term.write((_HOST and "\127" or "#"):rep(math.floor((easing - full) / 2)))
  771. term.setBackgroundColor(theme.visualiserBackground)
  772. term.setTextColor(theme.visualiserBar)
  773. term.write((_HOST and "\127" or "#"):rep(math.ceil((easing - full) / 2)))
  774. term.write((" "):rep(screenWidth - easing))
  775. end
  776.  
  777. local progressnub = math.floor((instance.tick / track.length) * (screenWidth - 14) + 0.5)
  778.  
  779. term.setCursorPos(1, screenHeight)
  780. term.setTextColor(theme.progressTime)
  781. term.setBackgroundColor(theme.progressBackground)
  782. term.write(formatTime(instance.tick / track.tempo))
  783.  
  784. term.setTextColor(theme.progressLine)
  785. term.write("\136")
  786. term.write(("\140"):rep(progressnub))
  787. term.setTextColor(theme.progressNub)
  788. term.setBackgroundColor(theme.progressNubBackground)
  789. term.write(theme.progressNubChar)
  790. term.setTextColor(theme.progressLine)
  791. term.setBackgroundColor(theme.progressBackground)
  792. term.write(("\140"):rep(screenWidth - 14 - progressnub))
  793. term.write("\132")
  794.  
  795. term.setTextColor(theme.progressTime)
  796. term.write(formatTime(track.length / track.tempo).." ")
  797. term.setTextColor(theme.progressButton)
  798. term.write(instance.playing and (_HOST and "|\016" or "|>") or "||")
  799. end
  800.  
  801. local function playSong(index)
  802. if index >= 1 and index <= #tracks then
  803. currentTrack = index
  804. track = tracks[currentTrack]
  805. context:removeInstance(1)
  806. instance = context:addInstance(track, 1, trackMode ~= 2, trackMode == 3)
  807. if currentTrack <= trackScroll then
  808. trackScroll = currentTrack - 1
  809. end
  810. if currentTrack > trackScroll + screenHeight - 7 then
  811. trackScroll = currentTrack - screenHeight + 7
  812. end
  813. drawStatic()
  814. end
  815. end
  816.  
  817. local function nextSong()
  818. if trackMode == 1 then
  819. playSong(currentTrack + 1)
  820. elseif trackMode == 4 then
  821. playSong(math.random(#tracks))
  822. end
  823. end
  824.  
  825. local function setScroll(scroll)
  826. trackScroll = scroll
  827. if trackScroll > #tracks - screenHeight + 7 then
  828. trackScroll = #tracks - screenHeight + 7
  829. end
  830. if trackScroll < 0 then
  831. trackScroll = 0
  832. end
  833. drawStatic()
  834. end
  835.  
  836. local function handleClick(x, y)
  837. if noUI then return end
  838. if y == 1 then
  839. if x == screenWidth then
  840. running = false
  841. elseif x >= screenWidth - 16 and x <= screenWidth - 2 and (x - screenWidth + 1) % 4 ~= 0 then
  842. trackMode = math.floor((x - screenWidth + 16) / 4) + 1
  843. instance.loop = trackMode == 3
  844. drawStatic()
  845. end
  846. elseif x < screenWidth and y >= 2 and y <= screenHeight - 6 then
  847. playSong(y - 1 + trackScroll)
  848. elseif x == screenWidth and y == 2 then
  849. setScroll(trackScroll - 2)
  850. elseif x == screenWidth and y == screenHeight - 6 then
  851. setScroll(trackScroll + 2)
  852. elseif x == screenWidth and y >= 3 and y <= screenHeight - 7 then
  853. setScroll(math.floor((y - 3) / (screenHeight - 10) * (#tracks - screenHeight + 7 ) + 0.5))
  854. elseif y == screenHeight then
  855. if x >= screenWidth - 1 and x <= screenWidth then
  856. instance.playing = not instance.playing
  857. elseif x >= 6 and x <= screenWidth - 8 then
  858. instance.tick = ((x - 6) / (screenWidth - 14)) * track.length
  859. end
  860. end
  861. end
  862.  
  863. local function handleScroll(x, y, scroll)
  864. if noUI then return end
  865. if y >= 2 and y <= screenHeight - 6 then
  866. setScroll(trackScroll + scroll * 2)
  867. end
  868. end
  869.  
  870. local function handleKey(key)
  871. if noInput then return end
  872. if key == keys.space then
  873. instance.playing = not instance.playing
  874. elseif key == keys.n then
  875. nextSong()
  876. elseif key == keys.p then
  877. playSong(currentTrack - 1)
  878. elseif key == keys.m then
  879. context.volume = (context.volume == 0) and 1 or 0
  880. elseif key == keys.left then
  881. instance.tick = instance.tick - track.tempo * 10
  882. if instance.tick < 1 then
  883. instance.tick = 1
  884. end
  885. elseif key == keys.right then
  886. instance.tick = instance.tick + track.tempo * 10
  887. elseif key == keys.up then
  888. context.volume = (context.volume == 1) and 1 or context.volume + 0.1
  889. elseif key == keys.down then
  890. context.volume = (context.volume == 0) and 0 or context.volume - 0.1
  891. elseif key == keys.j then
  892. setScroll(trackScroll + 2)
  893. elseif key == keys.k then
  894. setScroll(trackScroll - 2)
  895. elseif key == keys.pageUp then
  896. setScroll(trackScroll - 5)
  897. elseif key == keys.pageDown then
  898. setScroll(trackScroll + 5)
  899. elseif key == keys.leftShift then
  900. trackMode = trackMode % 4 + 1
  901. drawStatic()
  902. elseif key == keys.backspace then
  903. running = false
  904. end
  905. end
  906.  
  907. local function run()
  908. playSong(1)
  909. drawStatic()
  910. drawDynamic()
  911. local timer = os.startTimer(0.05)
  912. while running do
  913. local e = {os.pullEventRaw()}
  914. if e[1] == "timer" and e[2] == timer then
  915. timer = os.startTimer(0)
  916. local prevtick = instance.tick
  917. context:update()
  918. if prevtick > 1 and instance.tick == 1 then
  919. nextSong()
  920. end
  921. drawDynamic()
  922. elseif e[1] == "terminate" then
  923. running = false
  924. elseif e[1] == "term_resize" then
  925. screenWidth, screenHeight = term.getSize()
  926. elseif e[1] == "mouse_click" then
  927. handleClick(e[3], e[4])
  928. elseif e[1] == "mouse_scroll" then
  929. handleScroll(e[3], e[4], e[2])
  930. elseif e[1] == "key" then
  931. handleKey(e[2])
  932. end
  933. end
  934. end
  935.  
  936. local function exit()
  937. if noUI then return end
  938. term.setBackgroundColor(colors.black)
  939. term.setTextColor(colors.white)
  940. term.setCursorPos(1, 1)
  941. term.clear()
  942. end
  943.  
  944. init({...})
  945. run()
  946. exit()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement