Advertisement
Guest User

Untitled

a guest
Apr 4th, 2020
1,336
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. -- +---------------------+------------+---------------------+
  2. -- |                     |            |                     |
  3. -- |                     |  Note API  |                     |
  4. -- |                     |            |                     |
  5. -- +---------------------+------------+---------------------+
  6.  
  7. -- Note Block Song format + conversion tools:   David Norgren
  8. -- Iron Note Block + NBS loading & playback:   TheOriginalBIT
  9. -- Music player interface & API structure:         Bomb Bloke
  10.  
  11. -- ----------------------------------------------------------
  12.  
  13. -- Place Note Block Studio NBS files on your ComputerCraft computer,
  14. -- then play them back via a MoarPeripheral's Iron Note Block!
  15.  
  16. -- http://moarperipherals.com/index.php?title=Note
  17. -- http://www.computercraft.info/forums2/index.php?/topic/19357-moarperipherals
  18. -- http://www.minecraftforum.net/topic/136749-minecraft-note-block-studio
  19.  
  20. -- This script can be ran as any other, but it can *also* be loaded as an API!
  21. -- Doing so exposes the following functions:
  22.  
  23. -- < note.playSong(fileName) >
  24. -- Simply plays the specified NBS file.
  25.  
  26. -- < note.songEngine([fileName]) >
  27. -- Plays the optionally specified NBS file, but whether one is specified or not, does
  28. -- not return when complete - instead this is intended to run continuously as a background
  29. -- process. Launch it via the parallel API and run it alongside your own script!
  30.  
  31. --  While the song engine function is active, it can be manipulated by queuing the
  32. --  following events:
  33.  
  34. --   * musicPlay
  35. --   Add a filename in as a parameter to start playback, eg:
  36. --   os.queueEvent("musicPlay","mySong.nbs")
  37.  
  38. --   * musicPause
  39. --   Halts playback.
  40.  
  41. --   * musicResume
  42. --   Resumes playback.
  43.  
  44. --   * musicSkipTo
  45. --   Add a song position in as a parameter to skip to that segment. Specify the time
  46. --   in tenths of a second; for example, to skip a minute in use 600.
  47.  
  48. --   Additionally, whenever the song engine finishes a track, it will automatically
  49. --   throw a "musicFinished" event, or a "newTrack" event whenever a new song is loaded.
  50.  
  51. --  **Remember!** The API cannot respond to these events until YOUR code yields!
  52. --  Telling it to load a new song or jump to a different position won't take effect
  53. --  until you pull an event or something!
  54.  
  55. -- < note.setPeripheral(targetDevice1, [targetDevice2,] ...) >
  56. -- By default, upon loading the API attaches itself to any Iron Note Blocks it detects.
  57. -- Use this if you have specific note block(s) in mind, or wish to use different blocks
  58. -- at different times - perhaps mid-song! Returns true if at least one of the specified
  59. -- devices was valid, or false if none were.
  60.  
  61. --  **Note!** The Iron Note Block peripheral can currently play up to five instruments
  62. --            at any given moment. Providing multiple blocks to the API will cause it to
  63. --            automatically spread the load for those songs that need the extra notes.
  64. --            If you provide insufficient blocks, expect some notes to be skipped from
  65. --            certain songs.
  66.  
  67. --            Very few songs (if any) require more than two Iron Note Blocks.
  68.  
  69. -- < note.isPlaying() >
  70. -- Returns whether the API is currently mid-tune (ignoring whether it's paused or not).
  71.  
  72. -- < note.isPaused() >
  73. -- Returns whether playback is paused.
  74.  
  75. -- < note.getSongLength() >
  76. -- Returns the song length in "redstone updates". There are ten updates per second, or
  77. -- one per two game ticks.
  78.  
  79. -- < note.getSongPosition() >
  80. -- Returns the song position in "redstone updates". Time in game ticks is 2 * this. Time
  81. -- in seconds is this / 10.
  82.  
  83. -- < note.getSongSeconds() >
  84. -- Returns the song length in seconds.
  85.  
  86. -- < note.getSongPositionSeconds() >
  87. -- Returns the song position in seconds.
  88.  
  89. -- < note.getSongTempo() >
  90. -- Returns the song tempo, representing the "beats per second".
  91. -- Eg: 2.5 = one beat per 0.4 seconds.
  92. --       5 = one beat per 0.2 seconds.
  93. --      10 = one beat per 0.1 seconds.
  94.  
  95. -- ... or whatever the song happens to be set to.
  96. -- "Should" be a factor of ten, but lots of NBS files have other tempos.
  97.  
  98. -- < note.getSongName() >
  99. -- Returns the name of the song.
  100.  
  101. -- < note.getSongAuthor() >
  102. -- Returns the name of the NBS file author.
  103.  
  104. -- < note.getSongArtist() >
  105. -- Returns the name of the song artist.
  106.  
  107. -- < note.getSongDescription() >
  108. -- Returns the song's description.
  109.  
  110. --getvol, setvol, registerRemoteSpeaker
  111.  
  112. -- ----------------------------------------------------------
  113.  
  114. -- Cranking this value too high will cause crashes:
  115. local MAX_INSTRUMENTS_PER_NOTE_BLOCK = 5
  116.  
  117. local CCver
  118.  
  119. do
  120.     local vers = {{_HOST, tonumber(_HOST and _HOST:match("%d+.%d+"))}, {_CC_VERSION, tonumber(_CC_VERSION)},
  121.         {textutils.serializeJSON, 1.7}, {paintutils.drawBox, 1.64}, {fs.getDir, 1.63},
  122.         {term.current, 1.6}}
  123.  
  124.     for i = 1, #vers do if vers[i][1] then
  125.         CCver = vers[i][2]
  126.         break
  127.     end end
  128.  
  129.     if not CCver then error("Sorry, CC 1.6 or later is required.") end
  130. end
  131.  
  132. if not shell then
  133.     -- -----------------------
  134.     -- Load as API:
  135.     -- -----------------------
  136.    
  137.     local volume, paused, cTick, song, remote, haveVolume = 1
  138.    
  139.     local translate = {[0]=0,4,1,2,3}
  140.  
  141.     local noteName = {[0]="harp", "basedrum", "snare", "hat", "bass"}
  142.     local noteName2 = {[0]="base", "basedrum", "snare", "hat", "bass"}
  143.     -- Returns functions for each found "note block" attached; these take instrument/pitch/volume as their parameters.
  144.     function getNoteblocks()
  145.         local ironnote = {}
  146.         haveVolume = false
  147.        
  148.         if peripheral.find("iron_note") then
  149.             -- MoarPeripherals Iron Note Block
  150.             local ironnote = {peripheral.find("iron_note")}
  151.            
  152.             for i = 1, #ironnote do
  153.                 local func = ironnote[i].playNote
  154.                 ironnote[i] = function(inst, pitch, vol) func(inst, pitch) end
  155.             end
  156.  
  157.         elseif peripheral.find("speaker") then
  158.             -- ComputerCraft Speaker
  159.             ironnote = {peripheral.find("speaker")}
  160.             haveVolume = true
  161.             MAX_INSTRUMENTS_PER_NOTE_BLOCK = 20  -- Computronics crashes if spamming too many notes. Not sure what the safe limit is.
  162.  
  163.             for i = 1, #ironnote do
  164.                 local func = ironnote[i].playNote
  165.                 ironnote[i] = function(inst, pitch, vol) func(noteName2[inst], vol * 3, pitch) end
  166.             end
  167.  
  168.         elseif peripheral.find("iron_noteblock") then
  169.             -- Computronics Iron Note Block
  170.             ironnote = {peripheral.find("iron_noteblock")}
  171.             haveVolume = true
  172.             MAX_INSTRUMENTS_PER_NOTE_BLOCK = 20  -- Beats me what the real limit is here but 20 should be plenty anyways.
  173.  
  174.             for i = 1, #ironnote do
  175.                 local func = ironnote[i].playNote
  176.                 ironnote[i] = function(inst, pitch, vol) func(inst, pitch, vol / 10) end  -- NFI why volume works like this with Computronics.
  177.             end
  178.  
  179.         elseif peripheral.find("neuralInterface", function(side, periph) return periph.hasModule("minecraft:noteblock") end)
  180.                 or peripheral.find("Music")
  181.                 or peripheral.find("minecraft:noteblock") then
  182.                
  183.             -- Plethora Note Block module
  184.             ironnote = {peripheral.find("neuralInterface", function(side, periph) return periph.hasModule("minecraft:noteblock") end)}
  185.             if #ironnote == 0 then ironnote = {peripheral.find("Music")} end
  186.             if #ironnote == 0 then ironnote = {peripheral.find("minecraft:noteblock")} end
  187.            
  188.             haveVolume = true
  189.             MAX_INSTRUMENTS_PER_NOTE_BLOCK = 20  -- Beats me what the real limit is here but 20 should be plenty anyways.
  190.            
  191.             for i = 1, #ironnote do
  192.                 local play = ironnote[i].playNote
  193.                 ironnote[i] = function(inst, pitch, vol) if vol >= 0.1 then coroutine.resume(coroutine.create(play), inst, pitch, vol) end end  -- Don't ask
  194.             end
  195.  
  196.         elseif peripheral.find("note_block") then
  197.             -- OpenPeripherals Note Block
  198.             haveVolume = true
  199.             MAX_INSTRUMENTS_PER_NOTE_BLOCK = 20
  200.  
  201.             local comPitch = {0.5, 0.53, 0.56, 0.6, 0.63, 0.67, 0.7, 0.75, 0.8, 0.84, 0.9, 0.95, 1.0, 1.05, 1.1, 1.2, 1.25, 1.32, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0}
  202.            
  203.             ironnote = {peripheral.find("note_block")}
  204.            
  205.             local comSound = (_HOST and not _HOST:find("Minecraft 1.8")) and {[0] = "block.note.harp", "block.note.basedrum", "block.note.snare", "block.note.hat", "block.note.bass"} or {[0] = "note.harp", "note.bd", "note.snare", "note.hat", "note.bass"}
  206.            
  207.             for i = 1, #ironnote do
  208.                 local play = ironnote[i].playSound
  209.                 ironnote[i] = function(inst, pitch, vol) if vol >= 0.1 then play(comSound[inst], comPitch[pitch] or 0, vol) end end
  210.             end
  211.            
  212.         elseif commands then
  213.             -- Command Computer synth
  214.             haveVolume = true
  215.             MAX_INSTRUMENTS_PER_NOTE_BLOCK = 20
  216.  
  217.             local comPart = {}
  218.             for i = 0, 24 do comPart[i] = 1 / 24 * i end  -- If it looks wrong, that's not my bug: https://bugs.mojang.com/browse/MC-80658
  219.             local comPitch = {0.5, 0.53, 0.56, 0.6, 0.63, 0.67, 0.7, 0.75, 0.8, 0.84, 0.9, 0.95, 1.0, 1.05, 1.1, 1.2, 1.25, 1.32, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0}
  220.            
  221.             if _HOST and not _HOST:find("Minecraft 1.8") then  -- MC 1.9 or higher
  222.                 local comSound = {[0] = "block.note.harp", "block.note.basedrum", "block.note.snare", "block.note.hat", "block.note.bass"}
  223.                
  224.                 ironnote = {function(inst, pitch, vol)
  225.                     commands.execAsync("playsound " .. comSound[inst] .. " block @a ~ ~ ~ " .. tostring(vol) .. " " .. tostring(comPitch[pitch]))
  226.                     commands.execAsync("particle note ~ ~1 ~ " .. tostring(comPart[pitch]) .. " 0 0 1")
  227.                 end}
  228.             else
  229.                 local comSound = {[0] = "note.harp", "note.bd", "note.snare", "note.hat", "note.bass"}
  230.                
  231.                 ironnote = {function(inst, pitch, vol)
  232.                     commands.execAsync("playsound " .. comSound[inst] .. " @a ~ ~ ~ " .. tostring(vol) .. " " .. tostring(comPitch[pitch]))
  233.                     commands.execAsync("particle note ~ ~1 ~ " .. tostring(comPart[pitch]) .. " 0 0 1")
  234.                 end}
  235.             end
  236.         end
  237.        
  238.         return unpack(ironnote)
  239.     end
  240.    
  241.     local ironnote = {getNoteblocks()}
  242.    
  243.     local function assert(cdn, msg, lvl)
  244.         if not cdn then
  245.             error(msg or "assertion failed!", (lvl == 0 and 0 or lvl and (lvl + 1) or 2))
  246.         end
  247.         return cdn
  248.     end
  249.    
  250.     -- Returns a string ComputerCraft can render.
  251.     local safeString = CCver < 1.76 and
  252.             function(text)
  253.                 local newText = {}
  254.                 for i = 1, #text do
  255.                     local val = text:byte(i)
  256.                     newText[i] = (val > 31 and val < 127) and val or 63
  257.                 end
  258.                 return string.char(unpack(newText))
  259.             end
  260.         or function(text) return text end
  261.  
  262.     -- Returns the song length.
  263.     function getSongLength()
  264.         if type(song) == "table" then return song.length end
  265.     end
  266.    
  267.     -- Returns the song position.
  268.     function getSongPosition()
  269.         return cTick
  270.     end
  271.    
  272.     -- Returns the song length in seconds.
  273.     function getSongSeconds()
  274.         if type(song) == "table" then return song.length / song.tempo end
  275.     end
  276.    
  277.     -- Returns the song position in seconds.
  278.     function getSongPositionSeconds()
  279.         if type(song) == "table" then return cTick / song.tempo end
  280.     end
  281.    
  282.     -- Returns the tempo the song will be played at.
  283.     function getSongTempo()
  284.         if type(song) == "table" then return song.tempo end
  285.     end
  286.    
  287.     -- Switches to a different playback device.
  288.     function setPeripheral(...)
  289.         local newironnote = {}
  290.        
  291.         for i=1,#arg do if type(arg[i]) == "string" and peripheral.getType(arg[i]) == "iron_note" then
  292.             newironnote[#newironnote+1] = peripheral.wrap(arg[i])
  293.         elseif type(arg[i]) == "table" and arg[i].playNote then
  294.             newironnote[#newironnote+1] = arg[i]
  295.         end end
  296.        
  297.         if #newironnote > 0 then
  298.             ironnote = newironnote
  299.             for i = 1, #ironnote do ironnote[i] = ironnote[i].playNote end
  300.             return true
  301.         else return false end
  302.     end
  303.    
  304.     -- Switch to a remote playback device.
  305.     function registerRemoteSpeaker(speakerID)
  306.         if type(speakerID) ~= "number" and type(speakerID) ~= "string" then error("note.registerRemoteSpeaker: Expected: string (host name) or number (system ID). Got: "..type(speakerID)) end
  307.         speakerID = (not tonumber(speakerID)) and (rednet.lookup("MoarPNoteSpeaker", speakerID) or speakerID) or tonumber(speakerID)
  308.         if type(speakerID) ~= "number" then error("note.registerRemoteSpeaker: Rednet lookup failure on host \""..speakerID.."\".") end
  309.        
  310.         rednet.send(speakerID, "Hello?", "MoarPNoteSpeaker")
  311.         local incoming = {rednet.receive(10)}
  312.         if incoming[1] then haveVolume = incoming[2] else error("note.registerRemoteSpeaker: No reply from system ID "..speakerID..".", 2) end
  313.         remote = speakerID
  314.     end
  315.    
  316.     -- Returns whether music is loaded for playback.
  317.     function isPlaying()
  318.         return type(song) == "table"
  319.     end
  320.    
  321.     -- Returns whether playback is paused.
  322.     function isPaused()
  323.         return paused
  324.     end
  325.    
  326.     -- Returns the name of the song.
  327.     function getSongName()
  328.         if type(song) == "table" then return song.name end
  329.     end
  330.    
  331.     -- Returns the name of NBS file author.
  332.     function getSongAuthor()
  333.         if type(song) == "table" then return song.author end
  334.     end
  335.    
  336.     -- Returns the name of song artist.
  337.     function getSongArtist()
  338.         if type(song) == "table" then return song.originalauthor end
  339.     end
  340.    
  341.     -- Returns the song's description.
  342.     function getSongDescription()
  343.         if type(song) == "table" then return song.description end
  344.     end
  345.    
  346.     -- Gets the current volume level (between 0 and 1, inclusive).
  347.     function getVolumeLevel()
  348.         return haveVolume and volume
  349.     end
  350.    
  351.     -- Sets the current volume level (between 0 and 1, inclusive).
  352.     function setVolumeLevel(newVolume)
  353.         assert(type(newVolume) == "number", "note.setVolumeLevel(): Invalid argument.")
  354.        
  355.         if newVolume > 1 then
  356.             volume = 1
  357.         elseif newVolume < 0 then
  358.             volume = 0
  359.         else volume = newVolume end
  360.        
  361.         if remote then rednet.send(remote, volume, "MoarPNoteSpeaker") end
  362.     end
  363.    
  364.     local function byte_lsb(handle)
  365.         return assert(handle.read(), "Note NBS loading error: Unexpected EOF (end of file).", 2)
  366.     end
  367.  
  368.     local function int16_lsb(handle)
  369.         return bit.bor(bit.blshift(byte_lsb(handle), 8), byte_lsb(handle))
  370.     end
  371.  
  372.     local function int16_msb(handle)
  373.         local x = int16_lsb(handle)
  374.         --# convert little-endian to big-endian
  375.         local y = 0
  376.         y = y + bit.blshift(bit.band(x, 0x00FF), 8)
  377.         y = y + bit.brshift(bit.band(x, 0xFF00), 8)
  378.         return y
  379.     end
  380.  
  381.     local function int32_lsb(handle)
  382.         return bit.bor(bit.blshift(int16_lsb(handle), 16), int16_lsb(handle))
  383.     end
  384.  
  385.     local function int32_msb(handle)
  386.         local x = int32_lsb(handle)
  387.         --# convert little-endian to big-endian
  388.         local y = 0
  389.         y = y + bit.blshift(bit.band(x, 0x000000FF), 24)
  390.         y = y + bit.brshift(bit.band(x, 0xFF000000), 24)
  391.         y = y + bit.blshift(bit.band(x, 0x0000FF00), 8)
  392.         y = y + bit.brshift(bit.band(x, 0x00FF0000), 8)
  393.         return y
  394.     end
  395.  
  396.     local function nbs_string(handle)
  397.         local str = ""
  398.         for i = 1, int32_msb(handle) do
  399.             str = str..string.char(byte_lsb(handle))
  400.         end
  401.         return str
  402.     end
  403.  
  404.     local function readNbs(path)
  405.         assert(fs.exists(path), "Note NBS loading error: File \""..path.."\" not found. Did you forget to specify the containing folder?", 0)
  406.         assert(not fs.isDir(path), "Note NBS loading error: Specified file \""..path.."\" is actually a folder.", 0)
  407.         local handle = fs.open(path, "rb")
  408.  
  409.         local newSong = { notes = {}; }
  410.  
  411.         --# NBS format found on http://www.stuffbydavid.com/nbs
  412.         --# Part 1: Header
  413.         newSong.length = int16_msb(handle)
  414.         local layers = int16_msb(handle)
  415.         newSong.name = safeString(nbs_string(handle))
  416.         newSong.author = safeString(nbs_string(handle))
  417.         newSong.originalauthor = safeString(nbs_string(handle))
  418.         newSong.description = safeString(nbs_string(handle))
  419.         newSong.tempo = int16_msb(handle)/100
  420.        
  421.         byte_lsb(handle) --# auto-saving has been enabled (0 or 1)
  422.         byte_lsb(handle) --# The amount of minutes between each auto-save (if it has been enabled) (1-60)
  423.         byte_lsb(handle) --# The time signature of the song. If this is 3, then the signature is 3/4. Default is 4. This value ranges from 2-8
  424.         int32_msb(handle) --# The amount of minutes spent on the project
  425.         int32_msb(handle) --# The amount of times the user has left clicked
  426.         int32_msb(handle) --# The amount of times the user has right clicked
  427.         int32_msb(handle) --# The amount of times the user have added a block
  428.         int32_msb(handle) --# The amount of times the user have removed a block
  429.         nbs_string(handle) --# If the song has been imported from a .mid or .schematic file, that file name is stored here (Only the name of the file, not the path)
  430.  
  431.         --# Part 2: Note Blocks
  432.         local maxPitch = 24
  433.         local notes = newSong.notes
  434.         local tick = -1
  435.         local jumps = 0
  436.         while true do
  437.             jumps = int16_msb(handle)
  438.             if jumps == 0 then
  439.                 break
  440.             end
  441.             tick = tick + jumps
  442.             local layer = -1
  443.             while true do
  444.                 jumps = int16_msb(handle)
  445.                 if jumps == 0 then
  446.                     break
  447.                 end
  448.                 layer = layer + jumps
  449.                 local inst = byte_lsb(handle)
  450.                 local key = byte_lsb(handle)
  451.                 --
  452.                 notes[tick] = notes[tick] or {}
  453.                 table.insert(notes[tick], {inst = translate[inst]; pitch = math.max((key-33)%maxPitch,0); volume = layer})
  454.             end
  455.         end
  456.        
  457.         --# Part 3: Layers
  458.         local volume = {}
  459.         for i = 0, layers - 1 do
  460.             nbs_string(handle)
  461.             volume[i] = byte_lsb(handle) / 100
  462.         end
  463.         for i = 0, tick do if notes[i] then for _,note in pairs(notes[i]) do note.volume = volume[note.volume] or 1 end end end
  464.        
  465.         --# Part 4: Custom Instruments
  466.         --# Ignored at this time.
  467.        
  468.         handle.close()
  469.        
  470.         song = newSong
  471.     end
  472.  
  473.     function songEngine(targetSong)
  474.         assert(remote or ironnote[1] or commands, "Note songEngine failure: No Iron Note Blocks assigned.", 0)
  475.        
  476.         local haveVolume, tTick, curPeripheral, delay, notes, endTime = getVolumeLevel() ~= nil, os.startTimer(0.1), 1
  477.        
  478.         if targetSong then os.queueEvent("musicPlay",targetSong) end
  479.  
  480.         while true do
  481.             local e = { os.pullEvent() }
  482.            
  483.             if e[1] == "timer" and e[2] == tTick and song and not paused then
  484.                 if notes[cTick] then
  485.                     if not remote then
  486.                         local curMaxNotes, nowPlaying = (song.tempo == 20 and math.floor(MAX_INSTRUMENTS_PER_NOTE_BLOCK/2) or MAX_INSTRUMENTS_PER_NOTE_BLOCK) * #ironnote, 0
  487.                         for _,note in pairs(notes[cTick]) do
  488.                             ironnote[curPeripheral](note.inst, note.pitch, note.volume * volume)
  489.                             curPeripheral = (curPeripheral == #ironnote) and 1 or (curPeripheral + 1)
  490.                             nowPlaying = nowPlaying + 1
  491.                             if nowPlaying == curMaxNotes then break end
  492.                         end
  493.                     else rednet.send(remote, notes[cTick], "MoarPNoteSpeaker") end
  494.                 end
  495.                
  496.                 cTick = cTick + 1
  497.                
  498.                 if cTick > song.length then
  499.                     song = nil
  500.                     notes = nil
  501.                     cTick = nil
  502.                     os.queueEvent("musicFinished")
  503.                 else tTick = os.startTimer(endTime - (delay * (song.length + 1 - cTick)) - os.clock()) end
  504.                
  505.             elseif e[1] == "musicPause" then
  506.                 paused = true
  507.             elseif e[1] == "musicResume" then
  508.                 paused = false
  509.                 endTime = os.clock() + (delay * (song.length + 1 - cTick))
  510.                 tTick = os.startTimer(delay)
  511.             elseif e[1] == "musicSkipTo" then
  512.                 cTick = e[2]
  513.                 endTime = os.clock() + (delay * (song.length + 1 - cTick))
  514.             elseif e[1] == "musicPlay" then
  515.                 readNbs(e[2])
  516.                 notes = song.notes
  517.                 cTick = 0
  518.                 tTick = os.startTimer(0.1)
  519.                 paused = false
  520.                 delay = (100 / song.tempo) / 100
  521.                 endTime = os.clock() + (delay * (song.length + 1))
  522.                 os.queueEvent("newTrack")
  523.             end
  524.         end
  525.     end
  526.    
  527.     function playSong(targetSong)
  528.         parallel.waitForAny(function () songEngine(targetSong) end, function () os.pullEvent("musicFinished") end)
  529.     end
  530. else
  531.     -- -----------------------
  532.     -- Run as jukebox:
  533.     -- ------------------------------------------------------------
  534.    
  535.     -- Ignore everything below this point if you're only interested
  536.     -- in the API, unless you want to see example usage.
  537.    
  538.     -- ------------------------------------------------------------
  539.    
  540.     sleep(0)  -- 'cause ComputerCraft is buggy.
  541.    
  542.     os.loadAPI(shell.getRunningProgram())
  543.    
  544.     local startDir, playmode, lastSong, marqueePos, blackText, myEvent, bump, marquee, xSize, ySize, logo, buttons, termBlit, dot = shell.resolve("."), 0, {}, 1
  545.     local playInitials = {{"R", 8}, {"N", 6}, {"M", 5}}
  546.     local cursor = {{">>  ","  <<"},{"> > "," < <"},{" >> "," << "},{"> > "," < <"}}
  547.  
  548.     if term.blit then
  549.         termBlit = term.blit
  550.     else
  551.         local colourNum, hex = {}, "0123456789abcdef"
  552.         for i = 1, 16 do colourNum[hex:sub(i, i)] = 2 ^ (i - 1) end
  553.        
  554.         termBlit = function(t, tC, bC)
  555.             local curChars, curFG, curBG = {t:sub(1, 1)}, tC:sub(1, 1), bC:sub(1, 1)
  556.            
  557.             for i = 2, #t do
  558.                 local cT, cTC, cBC = t:sub(i, i), tC:sub(i, i), bC:sub(i, i)
  559.                
  560.                 if cTC ~= curFG or cBC ~= curBG then
  561.                     term.setTextColour(colourNum[curFG])
  562.                     term.setBackgroundColour(colourNum[curBG])
  563.                     term.write(table.concat(curChars))
  564.                     curChars, curFG, curBG = {cT}, cTC, cBC
  565.                 else curChars[#curChars + 1] = cT end
  566.             end
  567.  
  568.             term.setTextColour(colourNum[curFG])
  569.             term.setBackgroundColour(colourNum[curBG])
  570.             term.write(table.concat(curChars))
  571.         end
  572.     end
  573.    
  574.     if CCver >= 1.74 then  -- ToDo: If adding support for older CC vers, check this for pre-1.55
  575.         blackText = colours.grey
  576.     else
  577.         blackText = colours.black
  578.     end
  579.  
  580.     if CCver >= 1.76 then
  581.         logo = {{"\149\139\128\148\149\152\140\144\149\140\156\132\149\156\140\132",
  582.             "\149\149\137\149\149\149\128\149\149\128\149\128\149\151\131\128",
  583.             "\149\133\128\133\149\137\140\129\149\128\133\128\149\141\140\132"},
  584.             {"788b7bbb7bbb7bbb", "7bbb7b8b78b87bb8", "7b8b7bbb78b87bbb"},
  585.             {"8b88888888888888", "8888888888888888", "8888888888888888"}}
  586.        
  587.         buttons = {{{{"\151\128\135\148", "\149\145\128\149", "\138\128\139\133"},
  588.             {"\151\139\128\148", "\149\128\157\149", "\138\135\128\133"},
  589.             {"\151\128\148", "\149\128\149", "\138\128\133"},
  590.             {"\151\144\128", "\149\128\132", "\138\129\128"}},
  591.             {{"7773", "7733", "3733"}, {"7773", "7333", "3373"}, {"773", "773", "373"}, {"737", "733", "337"}},
  592.             {{"3737", "3337", "7777"}, {"3377", "3377", "7777"}, {"377", "377", "777"}, {"377", "337", "777"}}},
  593.            
  594.             {{{"\149\135\149", "\149\139\149"}, {"\149\139\149", "\149\135\149"}, {"\149\128\149", "\149\128\149"}, {"\149\130\144", "\149\159\129"}},
  595.             {{"773", "733"}, {"773", "733"}, {"773", "773"}, {"773", "733"}},
  596.             {{"337", "377"}, {"337", "377"}, {"377", "377"}, {"337", "377"}}}}
  597.        
  598.         dot = "\007"
  599.     else
  600.         logo = {{" | | +-+ --- +--", " |\\| | |  |  |- ", " | | +-+  |  +--"},
  601.             {"bbbbbbbbbbbbbbbb", "bbbbbbbbbbbbbbbb", "bbbbbbbbbbbbbbbb"},
  602.             {"7888788878887888", "7888788878887888", "7888788878887888"}}
  603.        
  604.         buttons = {{{{"| /|","|< |","| \\|"}, {"|\\ |","| >|","|/ |"}, {"| |","| |","| |"}, {"|\\ ","| >","|/ "}},
  605.             {{"3333", "3333", "3333"}, {"3333", "3333", "3333"}, {"333", "333", "333"}, {"333", "333", "333"}},
  606.             {{"7777", "7777", "7777"}, {"7777", "7777", "7777"}, {"777", "777", "777"}, {"777", "777", "777"}}},
  607.        
  608.             {{{"|/|","|\\|"}, {"|\\|","|/|"},{"| |","| |"}, {"|\\ ","|/ "}},
  609.             {{"333", "333"}, {"333", "333"}, {"333", "333"}, {"333", "333"}},
  610.             {{"777", "777"}, {"777", "777"}, {"777", "777"}, {"777", "777"}}}}
  611.        
  612.         dot = "O"
  613.     end
  614.    
  615.     if not term.isColour() then
  616.         local function monochrome(txt)
  617.             local result = {}
  618.             for i = 1, #txt do
  619.                 local char = txt:sub(i, i)
  620.                 result[i] = (CCver >= 1.74) and ((char == "3" or char == "b") and "0" or char) or ((char == "7" or char == "8") and "f" or "0")
  621.             end
  622.             return table.concat(result)
  623.         end
  624.        
  625.         for i = 1, 3 do logo[2][i], logo[3][i] = monochrome(logo[2][i]), monochrome(logo[3][i]) end
  626.        
  627.         for i = 1, 2 do for j = 2, 3 do for k = 1, 4 do
  628.             local thisTab = buttons[i][j][k]
  629.             for l = 1, #thisTab do thisTab[l] = monochrome(thisTab[l]) end
  630.         end end end
  631.     end
  632.    
  633.     -- Returns whether a click was performed at a given location.
  634.     -- If one parameter is passed, it checks to see if y is [1].
  635.     -- If two parameters are passed, it checks to see if x is [1] and y is [2].
  636.     -- If three parameters are passed, it checks to see if x is between [1]/[2] (non-inclusive) and y is [3].
  637.     -- If four paramaters are passed, it checks to see if x is between [1]/[2] and y is between [3]/[4] (non-inclusive).
  638.     local function clickedAt(...)
  639.         if myEvent[1] ~= "mouse_click" then return false end
  640.         if #arg == 1 then return (arg[1] == myEvent[4])
  641.         elseif #arg == 2 then return (myEvent[3] == arg[1] and myEvent[4] == arg[2])
  642.         elseif #arg == 3 then return (myEvent[3] > arg[1] and myEvent[3] < arg[2] and myEvent[4] == arg[3])
  643.         else return (myEvent[3] > arg[1] and myEvent[3] < arg[2] and myEvent[4] > arg[3] and myEvent[4] < arg[4]) end
  644.     end
  645.  
  646.     -- Returns whether one of a given set of keys was pressed.
  647.     local function pressedKey(...)
  648.         if myEvent[1] ~= "key" then return false end
  649.         for i=1,#arg do if arg[i] == myEvent[2] then return true end end
  650.         return false
  651.     end
  652.  
  653.     -- Ensures the display is suitable for play.
  654.     local function enforceScreenSize()
  655.         term.setTextColor(colours.white)
  656.         term.setBackgroundColor(colours.black)
  657.        
  658.         while true do
  659.             xSize, ySize = term.getSize()
  660.             term.clear()
  661.            
  662.             if xSize < 26 or ySize < 7 then
  663.                 term.setCursorPos(1,1)
  664.                 print("Display too small!\n")
  665.                 local myEvent = {os.pullEvent()}
  666.                 if myEvent[1] == "mouse_click" or myEvent[1] == "key" then error() end
  667.             else return end
  668.         end
  669.     end
  670.  
  671.     local function drawPlaymode()
  672.         term.setBackgroundColour(term.isColour() and colours.lightGrey or colours.black)
  673.         term.setTextColour(term.isColour() and colours.black or colours.white)
  674.        
  675.         if xSize > 49 then
  676.             term.setCursorPos(bump+34, 1)
  677.             term.write("[R]epeat    ( )")
  678.             term.setCursorPos(bump+34, 2)
  679.             term.write("Auto-[N]ext ( )")
  680.             term.setCursorPos(bump+34, 3)
  681.             term.write("[M]ix       ( )")
  682.  
  683.             if playmode ~= 0 then
  684.                 term.setTextColour(term.isColour() and colours.blue or colours.white)
  685.                 term.setCursorPos(bump+47, playmode)
  686.                 term.write(dot)
  687.             end
  688.         else
  689.             term.setCursorPos(xSize - 12, 1)
  690.             term.write("   [R]epeat")
  691.             term.setCursorPos(xSize - 12, 2)
  692.             term.write("Auto-[N]ext")
  693.             term.setCursorPos(xSize - 12, 3)
  694.             term.write("      [M]ix")
  695.            
  696.             if playmode ~= 0 then
  697.                 term.setTextColour(term.isColour() and colours.blue or colours.black)
  698.                 if not term.isColour() then term.setBackgroundColour(colours.white) end
  699.                 term.setCursorPos(xSize - playInitials[playmode][2], playmode)
  700.                 term.write(playInitials[playmode][1])
  701.             end
  702.         end
  703.     end
  704.    
  705.     local function drawVolumeBar()
  706.         term.setCursorPos(9,ySize-2)
  707.         term.setBackgroundColour(term.isColour() and colours.brown or colours.black)
  708.         term.setTextColour(term.isColour() and colours.red or colours.white)
  709.         term.write("-")
  710.        
  711.         term.setBackgroundColour(term.isColour() and colours.white or colours.black)
  712.         term.setTextColour(term.isColour() and colours.lightGrey or blackText)
  713.         term.write(string.rep("V", xSize - 18))
  714.        
  715.         term.setBackgroundColour(term.isColour() and colours.green or colours.black)
  716.         term.setTextColour(term.isColour() and colours.lime or colours.white)
  717.         term.write("+")
  718.        
  719.         term.setTextColour(term.isColour() and colours.blue or colours.white)
  720.         term.setBackgroundColour(colours.black)
  721.         term.setCursorPos(10+(xSize-19)*note.getVolumeLevel(),ySize-2)
  722.         term.write(dot)
  723.     end
  724.    
  725.     local function drawInterface()
  726.         if term.isColour() then
  727.             -- Header / footer.
  728.             term.setBackgroundColour(colours.grey)
  729.             for i = 1, 3 do
  730.                 term.setCursorPos(1,i)
  731.                 term.clearLine()
  732.                 term.setCursorPos(1,ySize-i+1)
  733.                 term.clearLine()
  734.             end
  735.            
  736.             -- Quit button.
  737.             term.setTextColour(colours.white)
  738.             term.setBackgroundColour(colours.red)
  739.             term.setCursorPos(xSize,1)
  740.             term.write("X")
  741.         end
  742.  
  743.         if xSize > 49 then
  744.             -- Note logo.
  745.             local t, tC, bC = logo[1], logo[2], logo[3]
  746.             for i = 1, 3 do
  747.                 term.setCursorPos(1, i)
  748.                 termBlit(t[i], tC[i], bC[i])
  749.             end
  750.            
  751.             -- Skip back / forward buttons.
  752.             t, tC, bC = buttons[1][1], buttons[1][2], buttons[1][3]
  753.             for j = 0, 1 do for i = 1, 3 do
  754.                 term.setCursorPos(bump + 17 + j * 11, i)
  755.                 termBlit(t[j+1][i], tC[j+1][i], bC[j+1][i])
  756.             end end
  757.         else
  758.             -- Note logo.
  759.             term.setTextColour(term.isColour() and colours.blue or colours.white)
  760.             term.setBackgroundColour(term.isColour() and colours.lightGrey or colours.black)
  761.             term.setCursorPos(3, 1)
  762.             term.write(" N O T E ")
  763.    
  764.             -- Skip back / forward buttons.
  765.             local t, tC, bC = buttons[2][1], buttons[2][2], buttons[2][3]
  766.             for j = 0, 1 do for i = 1, 2 do
  767.                 term.setCursorPos(2 + j * 8, i + 1)
  768.                 termBlit(t[j+1][i], tC[j+1][i], bC[j+1][i])
  769.             end end
  770.         end
  771.        
  772.         -- Progress bar.
  773.         term.setCursorPos(2,ySize-1)
  774.         term.setTextColour(term.isColour() and colours.black or colours.white)
  775.         term.setBackgroundColour(term.isColour() and colours.lightGrey or colours.black)
  776.         term.write("|"..string.rep("=",xSize-4).."|")
  777.        
  778.         drawPlaymode()
  779.         if note.getVolumeLevel() then drawVolumeBar() end
  780.     end
  781.    
  782.     local function startSong(newSong)
  783.         if #lastSong == 32 then lastSong[32] = nil end
  784.         table.insert(lastSong,1,newSong)
  785.         os.queueEvent("musicPlay",newSong)
  786.         marquee = nil
  787.         marqueePos = 1
  788.     end
  789.    
  790.     local function noteMenu()
  791.         local lastPauseState, dragX = "maybe"
  792.         bump = math.floor((xSize - 49) / 2) + 1
  793.         drawInterface()
  794.        
  795.         while true do
  796.             local displayList, position, lastPosition, animationTimer, curCount, gapTimer, lastProgress = {}, 1, 0, os.startTimer(0), 1
  797.             if #shell.resolve(".") > 0 then displayList[1] = ".." end
  798.  
  799.             do
  800.                 local fullList = fs.list(shell.resolve("."))
  801.                 table.sort(fullList, function (a, b) return string.lower(a) < string.lower(b) end)
  802.                 for i = 1, #fullList do if fs.isDir(shell.resolve(fullList[i])) then displayList[#displayList + 1] = fullList[i] end end
  803.                 for i = 1, #fullList do if fullList[i]:sub(#fullList[i]-3):lower() == ".nbs" then displayList[#displayList + 1] = fs.getName(fullList[i]) end end
  804.             end
  805.  
  806.             while true do
  807.                 myEvent = {os.pullEvent()}
  808.                
  809.                 if myEvent[1] == "mouse_click" then dragX = (myEvent[4] == ySize) and myEvent[3] or nil end
  810.                
  811.                 -- Track animations (bouncing, function (a, b) return string.lower(a) < string.lower(b) end cursor + scrolling marquee).
  812.                 if myEvent[1] == "timer" and myEvent[2] == animationTimer then
  813.                     if marquee then marqueePos = marqueePos == #marquee and 1 or (marqueePos + 1) end
  814.                     curCount = curCount == 4 and 1 or (curCount + 1)
  815.                     animationTimer = os.startTimer(0.5)
  816.                     myEvent[1] = "cabbage"
  817.                    
  818.                 -- Queue a new song to start playing, based on the playmode toggles (or if the user clicked the skip-ahead button).
  819.                 elseif (myEvent[1] == "timer" and myEvent[2] == gapTimer and not note.isPlaying()) or (pressedKey(keys.d,keys.right) or (xSize > 49 and clickedAt(bump+27,bump+32,0,4) or clickedAt(9,13,1,4))) then
  820.                     if playmode == 1 then
  821.                         os.queueEvent("musicPlay",lastSong[1])
  822.                     elseif (playmode == 2 or (playmode == 0 and myEvent[1] ~= "timer")) and not fs.isDir(shell.resolve(displayList[#displayList])) then
  823.                         if shell.resolve(displayList[position]) == lastSong[1] or fs.isDir(shell.resolve(displayList[position])) then repeat
  824.                             position = position + 1
  825.                             if position > #displayList then position = 1 end
  826.                         until not fs.isDir(shell.resolve(displayList[position])) end
  827.                        
  828.                         startSong(shell.resolve(displayList[position]))
  829.                     elseif playmode == 3 and not fs.isDir(shell.resolve(displayList[#displayList])) then
  830.                         repeat position = math.random(#displayList) until not fs.isDir(shell.resolve(displayList[position]))
  831.                         startSong(shell.resolve(displayList[position]))
  832.                     end
  833.                    
  834.                     gapTimer = nil
  835.                     myEvent[1] = "cabbage"
  836.                
  837.                 elseif myEvent[1] ~= "timer" then   -- Special consideration, bearing in mind that the songEngine is spamming ten such events a second...
  838.                     -- Move down the list.
  839.                     if pressedKey(keys.down,keys.s) or (myEvent[1] == "mouse_scroll" and myEvent[2] == 1) then
  840.                         position = position == #displayList and 1 or (position + 1)
  841.  
  842.                     -- Move up the list.
  843.                     elseif pressedKey(keys.up,keys.w) or (myEvent[1] == "mouse_scroll" and myEvent[2] == -1) then
  844.                         position = position == 1 and #displayList or (position - 1)
  845.  
  846.                     -- Start a new song.
  847.                     elseif pressedKey(keys.enter, keys.space) or ((xSize > 49 and clickedAt(bump+22,bump+26,0,4) or clickedAt(5,9,1,4)) and not note.isPlaying()) or clickedAt(math.floor(ySize / 2) + 1) then
  848.                         if fs.isDir(shell.resolve(displayList[position])) then
  849.                             shell.setDir(shell.resolve(displayList[position]))
  850.                             break
  851.                         else startSong(shell.resolve(displayList[position])) end
  852.  
  853.                     -- User clicked somewhere on the file list; move that entry to the currently-selected position.
  854.                     elseif clickedAt(0, xSize + 1, 3, ySize - 2) then
  855.                         position = position + myEvent[4] - math.floor(ySize / 2) - 1
  856.                         position = position > #displayList and #displayList or position
  857.                         position = position < 1 and 1 or position
  858.  
  859.                     -- Respond to a screen-resize; triggers a full display redraw.
  860.                     elseif myEvent[1] == "term_resize" or myEvent[1] == "monitor_resize" then
  861.                         enforceScreenSize()
  862.                         bump = math.floor((xSize - 49) / 2) + 1
  863.                         lastPosition = 0
  864.                         drawInterface()
  865.                         animationTimer = os.startTimer(0)
  866.                         lastPauseState = "maybe"
  867.  
  868.                     -- Quit.
  869.                     elseif pressedKey(keys.q, keys.x, keys.t) or clickedAt(xSize, 1) then
  870.                         if myEvent[1] == "key" then os.pullEvent("char") end
  871.                         os.unloadAPI("note")
  872.                         term.setTextColour(colours.white)
  873.                         term.setBackgroundColour(colours.black)
  874.                         term.clear()
  875.                         term.setCursorPos(1,1)
  876.                         print("Thanks for using the Note NBS player!\n")
  877.                         shell.setDir(startDir)
  878.                         error()
  879.  
  880.                     -- Toggle repeat mode.
  881.                     elseif pressedKey(keys.r) or (xSize > 49 and clickedAt(bump + 33, bump + 49, 1)) or (xSize < 50 and clickedAt(xSize - 13, xSize - 1, 1)) then
  882.                         playmode = playmode == 1 and 0 or 1
  883.                         drawPlaymode()
  884.  
  885.                     -- Toggle auto-next mode.
  886.                     elseif pressedKey(keys.n) or (xSize > 49 and clickedAt(bump + 33, bump + 49, 2)) or (xSize < 50 and clickedAt(xSize - 13, xSize - 1, 2)) then
  887.                         playmode = playmode == 2 and 0 or 2
  888.                         drawPlaymode()
  889.  
  890.                     -- Toggle mix (shuffle) mode.
  891.                     elseif pressedKey(keys.m) or (xSize > 49 and clickedAt(bump + 33, bump + 49, 3)) or (xSize < 50 and clickedAt(xSize - 13, xSize - 1, 3)) then
  892.                         playmode = playmode == 3 and 0 or 3
  893.                         drawPlaymode()
  894.  
  895.                     -- Music finished; wait a second or two before responding.
  896.                     elseif myEvent[1] == "musicFinished" then
  897.                         gapTimer = os.startTimer(2)
  898.                         lastPauseState = "maybe"
  899.                         marquee = ""
  900.  
  901.                     -- Skip back to start of the song (or to the previous song, if the current song just started).
  902.                     elseif pressedKey(keys.a,keys.left) or (xSize > 49 and clickedAt(bump+16,bump+21,0,4) or clickedAt(1,5,1,4)) then
  903.                         if note.isPlaying() and note.getSongPositionSeconds() > 3 then
  904.                             os.queueEvent("musicSkipTo",0)
  905.                             os.queueEvent("musicResume")
  906.                         elseif #lastSong > 1 then
  907.                             table.remove(lastSong,1)
  908.                             startSong(table.remove(lastSong,1))
  909.                         end
  910.  
  911.                     -- Toggle pause/resume.
  912.                     elseif note.isPlaying() and (pressedKey(keys.p) or (xSize > 49 and clickedAt(bump+22,bump+26,0,4) or clickedAt(5,9,1,4))) then
  913.                         if note.isPaused() then os.queueEvent("musicResume") else os.queueEvent("musicPause") end
  914.  
  915.                     -- Tracking bar clicked.
  916.                     elseif note.isPlaying() and (myEvent[1] == "mouse_click" or myEvent[1] == "mouse_drag") and myEvent[3] > 1 and myEvent[3] < xSize and myEvent[4] == ySize - 1 then
  917.                         os.queueEvent("musicSkipTo",math.floor(note.getSongLength()*(myEvent[3]-1)/(xSize-2)))
  918.                    
  919.                     -- Song engine just initiated a new track.
  920.                     elseif myEvent[1] == "newTrack" then
  921.                         marquee = " [Title: "
  922.                         if note.getSongName() ~= "" then marquee = marquee..note.getSongName().."]" else marquee = marquee..fs.getName(lastSong[1]).."]" end
  923.                         if note.getSongArtist() ~= "" then marquee = marquee.." [Artist: "..note.getSongArtist().."]" end
  924.                         if note.getSongAuthor() ~= "" then marquee = marquee.." [NBS Author: "..note.getSongAuthor().."]" end
  925.                         marquee = marquee.." [Tempo: "..note.getSongTempo().."]"
  926.                         if note.getSongDescription() ~= "" then marquee = marquee.." [Description: "..note.getSongDescription().."]" end
  927.                         lastPauseState = "maybe"
  928.                    
  929.                     -- Drag the marquee.
  930.                     elseif myEvent[1] == "mouse_drag" and myEvent[4] == ySize and dragX and marquee then
  931.                         marqueePos = (marqueePos - myEvent[3] + dragX)%#marquee
  932.                         dragX = myEvent[3]
  933.                    
  934.                     elseif note.getVolumeLevel() then
  935.                         -- Volume down.
  936.                         if pressedKey(keys.minus,keys.underscore,keys.numPadSubtract) or clickedAt(9,ySize-2) then
  937.                             note.setVolumeLevel(note.getVolumeLevel()-0.05)
  938.                             drawVolumeBar()
  939.  
  940.                         -- Volume up.
  941.                         elseif pressedKey(keys.plus,keys.equals,keys.numPadAdd) or clickedAt(xSize-8,ySize-2) then
  942.                             note.setVolumeLevel(note.getVolumeLevel()+0.05)
  943.                             drawVolumeBar()
  944.  
  945.                         -- Volume bar clicked.
  946.                         elseif (myEvent[1] == "mouse_click" or myEvent[1] == "mouse_drag") and myEvent[3] > 9 and myEvent[3] < xSize-8 and myEvent[4] == ySize - 2 then
  947.                             note.setVolumeLevel((myEvent[3]-10)/(xSize-19))
  948.                             drawVolumeBar()
  949.                         end
  950.                     end
  951.                 end
  952.                
  953.                 -- Play / pause button.
  954.                 if lastPauseState ~= note.isPaused() then
  955.                     local ind = (note.isPlaying() and not note.isPaused()) and 3 or 4
  956.                     if xSize > 49 then
  957.                         local t, tC, bC = buttons[1][1][ind], buttons[1][2][ind], buttons[1][3][ind]
  958.                         for i=1,3 do
  959.                             term.setCursorPos(bump + 23,i)
  960.                             termBlit(t[i], tC[i], bC[i])
  961.                         end
  962.                     else
  963.                         local t, tC, bC = buttons[2][1][ind], buttons[2][2][ind], buttons[2][3][ind]
  964.                         for i=1,2 do
  965.                             term.setCursorPos(6,i+1)
  966.                             termBlit(t[i], tC[i], bC[i])
  967.                         end
  968.                     end
  969.                     lastPauseState = note.isPaused()
  970.                 end
  971.                
  972.                 -- Update other screen stuff.
  973.                 if myEvent[1] ~= "timer" then
  974.                     term.setTextColour(term.isColour() and colours.black or colours.white)
  975.                     term.setBackgroundColour(term.isColour() and colours.lightGrey or colours.black)
  976.                    
  977.                     -- Clear old progress bar position.
  978.                     if lastProgress then
  979.                         term.setCursorPos(lastProgress,ySize-1)
  980.                         term.write((lastProgress == 2 or lastProgress == xSize - 1) and "|" or "=")
  981.                         lastProgress = nil
  982.                     end
  983.  
  984.                     -- Song timers.
  985.                     if note.isPlaying() then
  986.                         term.setCursorPos(xSize-5,ySize-2)
  987.                        
  988.                         local mins = tostring(math.min(99,math.floor(note.getSongSeconds()/60)))
  989.                         local secs = tostring(math.floor(note.getSongSeconds()%60))
  990.                         term.write((#mins > 1 and "" or "0")..mins..":"..(#secs > 1 and "" or "0")..secs)
  991.  
  992.                         term.setCursorPos(2,ySize-2)
  993.                         if note.isPaused() and bit.band(curCount,1) == 1 then
  994.                             term.write("     ")
  995.                         else
  996.                             mins = tostring(math.min(99,math.floor(note.getSongPositionSeconds()/60)))
  997.                             secs = tostring(math.floor(note.getSongPositionSeconds()%60))
  998.                             term.write((#mins > 1 and "" or "0")..mins..":"..(#secs > 1 and "" or "0")..secs)
  999.                         end
  1000.  
  1001.                         -- Progress bar position.
  1002.                         term.setTextColour(term.isColour() and colours.blue or colours.white)
  1003.                         term.setBackgroundColour(colours.black)
  1004.                         lastProgress = 2+math.floor(((xSize-3) * note.getSongPosition() / note.getSongLength()))
  1005.                         term.setCursorPos(lastProgress,ySize-1)
  1006.                         term.write(dot)
  1007.                     else
  1008.                         term.setCursorPos(2,ySize-2)
  1009.                         term.write("00:00")
  1010.                         term.setCursorPos(xSize-5,ySize-2)
  1011.                         term.write("00:00")
  1012.                     end
  1013.  
  1014.                     -- Scrolling marquee.
  1015.                     if marquee then
  1016.                         term.setTextColour(term.isColour() and colours.black or colours.white)
  1017.                         term.setBackgroundColour(term.isColour() and colours.grey or colours.black)
  1018.                         term.setCursorPos(1,ySize)
  1019.  
  1020.                         if marquee == "" then
  1021.                             term.clearLine()
  1022.                             marquee = nil
  1023.                         else
  1024.                             local thisLine = marquee:sub(marqueePos,marqueePos+xSize-1)
  1025.                             while #thisLine < xSize do thisLine = thisLine..marquee:sub(1,xSize-#thisLine) end
  1026.                             term.write(thisLine)
  1027.                         end
  1028.                     end
  1029.                    
  1030.                     -- File list.
  1031.                     term.setBackgroundColour(colours.black)
  1032.                     for y = position == lastPosition and (math.floor(ySize / 2)+1) or 4, position == lastPosition and (math.floor(ySize / 2)+1) or (ySize - 3) do
  1033.                         local thisLine = y + position - math.floor(ySize / 2) - 1
  1034.  
  1035.                         if displayList[thisLine] then
  1036.                             local thisString = displayList[thisLine]
  1037.                             thisString = fs.isDir(shell.resolve(thisString)) and "["..thisString.."]" or thisString:sub(1,#thisString-4)
  1038.  
  1039.                             if thisLine == position then
  1040.                                 term.setCursorPos(math.floor((xSize - #thisString - 8) / 2)+1, y)
  1041.                                 term.clearLine()
  1042.                                 term.setTextColour(term.isColour() and colours.cyan or blackText)
  1043.                                 term.write(cursor[curCount][1])
  1044.                                 term.setTextColour(term.isColour() and colours.blue or colours.white)
  1045.                                 term.write(thisString)
  1046.                                 term.setTextColour(term.isColour() and colours.cyan or blackText)
  1047.                                 term.write(cursor[curCount][2])
  1048.                             else
  1049.                                 term.setCursorPos(math.floor((xSize - #thisString) / 2)+1, y)
  1050.                                 term.clearLine()
  1051.  
  1052.                                 if y == 4 or y == ySize - 3 then
  1053.                                     term.setTextColour(blackText)
  1054.                                 elseif y == 5 or y == ySize - 4 then
  1055.                                     term.setTextColour(term.isColour() and colours.grey or blackText)
  1056.                                 elseif y == 6 or y == ySize - 5 then
  1057.                                     term.setTextColour(term.isColour() and colours.lightGrey or colours.white)
  1058.                                 else term.setTextColour(colours.white) end
  1059.  
  1060.                                 term.write(thisString)
  1061.                             end
  1062.                         else
  1063.                             term.setCursorPos(1,y)
  1064.                             term.clearLine()
  1065.                         end
  1066.                     end
  1067.  
  1068.                     lastPosition = position
  1069.                 end
  1070.             end
  1071.         end
  1072.     end
  1073.    
  1074.     local function beBluetoothSpeaker()
  1075.         peripheral.find("modem", rednet.open)
  1076.        
  1077.         local myName = (not os.getComputerLabel()) and ("Speaker"..math.random(10000)) or os.getComputerLabel()
  1078.         rednet.host("MoarPNoteSpeaker", myName)
  1079.         print("Hosting remote speaker service as \"" .. myName .. "\".")
  1080.        
  1081.         local haveVolume, ironnote, curPeripheral, nowPlaying, tick = note.getVolumeLevel() ~= nil, {note.getNoteblocks()}, 1, 0
  1082.        
  1083.         local x, y = term.getCursorPos()
  1084.         term.write(#ironnote .. " speaker(s) available.")
  1085.        
  1086.         while true do
  1087.             while #ironnote == 0 do
  1088.                 os.pullEvent("peripheral")
  1089.                 ironnote = {peripheral.find("iron_note")}
  1090.                 for i = 1, #ironnote do ironnote[i] = ironnote[i].playNote end
  1091.                 term.setCursorPos(1, y)
  1092.                 term.clearLine()
  1093.                 term.write(#ironnote .. " speaker(s) available.")
  1094.             end
  1095.            
  1096.             local myEvent = {os.pullEventRaw()}
  1097.            
  1098.             if myEvent[1] == "rednet_message" and myEvent[4] == "MoarPNoteSpeaker" then
  1099.                 if type(myEvent[3]) == "table" then
  1100.                     for _,note in pairs(myEvent[3]) do
  1101.                         if nowPlaying == MAX_INSTRUMENTS_PER_NOTE_BLOCK * #ironnote then break end
  1102.                         ironnote[curPeripheral](note.inst, note.pitch, note.volume)
  1103.                         curPeripheral = (curPeripheral == #ironnote) and 1 or (curPeripheral + 1)
  1104.                         nowPlaying = nowPlaying + 1
  1105.                     end
  1106.                     if not tick then tick = os.startTimer(0.1) end
  1107.                 elseif type(myEvent[3]) == "number" then
  1108.                     note.setVolumeLevel(myEvent[3])
  1109.                 else rednet.send(myEvent[2], note.getVolumeLevel()) end
  1110.             elseif myEvent[1] == "peripheral_detach" or myEvent[1] == "peripheral" then
  1111.                 ironnote = {peripheral.find("iron_note")}
  1112.                 for i = 1, #ironnote do ironnote[i] = ironnote[i].playNote end
  1113.                 if curPeripheral > #ironnote then curPeripheral = 1 end
  1114.                 term.setCursorPos(1, y)
  1115.                 term.clearLine()
  1116.                 term.write(#ironnote .. " speaker(s) available.")
  1117.             elseif myEvent[1] == "timer" and myEvent[2] == tick then
  1118.                 nowPlaying = 0
  1119.                 tick = nil
  1120.             elseif myEvent[1] == "terminate" then
  1121.                 rednet.unhost("MoarPNoteSpeaker", myName)
  1122.                 print()
  1123.                 error()
  1124.             end
  1125.         end
  1126.     end
  1127.    
  1128.     local function listBluetoothSpeakers()
  1129.         peripheral.find("modem", rednet.open)
  1130.            
  1131.         local servers = {rednet.lookup("MoarPNoteSpeaker")}
  1132.        
  1133.         print("Available Note servers:")
  1134.         if #servers == 0 then print("(None)") end
  1135.         for i=1,#servers do print(servers[i]) end
  1136.         error()
  1137.     end
  1138.    
  1139.     local function pairBluetoothSpeaker(speakerID)
  1140.         peripheral.find("modem", rednet.open)
  1141.         note.registerRemoteSpeaker(speakerID)
  1142.     end
  1143.    
  1144.     do local args = {...}
  1145.     for i=1,#args do if not args[i] then
  1146.         break
  1147.     elseif args[i]:lower() == "-r" then
  1148.         playmode = 1
  1149.     elseif args[i]:lower() == "-n" then
  1150.         playmode = 2
  1151.     elseif args[i]:lower() == "-m" then
  1152.         playmode = 3
  1153.     elseif fs.isDir(shell.resolve(args[i])) then
  1154.         shell.setDir(shell.resolve(args[i]))
  1155.     elseif fs.isDir(args[i]) then
  1156.         shell.setDir(args[i])
  1157.     elseif fs.exists(shell.resolve(args[i])) then
  1158.         local filePath = shell.resolve(args[i])
  1159.         shell.setDir(fs.getDir(filePath))
  1160.         startSong(filePath)
  1161.     elseif fs.exists(shell.resolve(args[i]..".nbs")) then
  1162.         local filePath = shell.resolve(args[i]..".nbs")
  1163.         shell.setDir(fs.getDir(filePath))
  1164.         startSong(filePath)
  1165.     elseif fs.exists(args[i]) then
  1166.         shell.setDir(fs.getDir(args[i]))
  1167.         startSong(args[i])
  1168.     elseif fs.exists(args[i]..".nbs") then
  1169.         shell.setDir(fs.getDir(args[i]))
  1170.         startSong(args[i]..".nbs")
  1171.     elseif args[i]:lower() == "-server" then
  1172.         beBluetoothSpeaker()
  1173.     elseif args[i]:lower() == "-list" then
  1174.         listBluetoothSpeakers()
  1175.     elseif args[i]:lower() == "-remote" then
  1176.         pairBluetoothSpeaker(table.remove(args, i + 1))
  1177.     end end end
  1178.    
  1179.     if playmode > 1 then os.queueEvent("musicFinished") end
  1180.    
  1181.     enforceScreenSize()
  1182.     return parallel.waitForAny(note.songEngine, noteMenu)
  1183. end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement