asdaa0

Note

Feb 2nd, 2017
234
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 37.53 KB | None | 0 0
  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. if not shell then
  118.     -- -----------------------
  119.     -- Load as API:
  120.     -- -----------------------
  121.    
  122.     local ironnote, volume, paused, cTick, song, remote, haveVolume = {peripheral.wrap("right")}, 1
  123.     if ironnote[1] then haveVolume = ironnote[1].playSfx ~= nil end
  124.     for i = 1, #ironnote do ironnote[i] = ironnote[i].playNote end
  125.    
  126.     local translate = {[0]=0,4,1,2,3}
  127.    
  128.     local function assert(cdn, msg, lvl)
  129.         if not cdn then
  130.             error(msg or "assertion failed!", (lvl == 0 and 0 or lvl and (lvl + 1) or 2))
  131.         end
  132.         return cdn
  133.     end
  134.    
  135.     -- Returns a string ComputerCraft can render. Just in case this isn't fixed:
  136.     -- http://www.computercraft.info/forums2/index.php?/topic/16882-computercraft-beta-versions-bug-reports/page__view__findpost__p__212628
  137.     local function safeString(text)
  138.         local newText = {}
  139.         for i = 1, #text do
  140.             local val = text:byte(i)
  141.             newText[i] = (val > 31 and val < 127) and val or 63
  142.         end
  143.         return string.char(unpack(newText))
  144.     end
  145.  
  146.     -- Returns the song length.
  147.     function getSongLength()
  148.         if type(song) == "table" then return song.length end
  149.     end
  150.    
  151.     -- Returns the song position.
  152.     function getSongPosition()
  153.         return cTick
  154.     end
  155.    
  156.     -- Returns the song length in seconds.
  157.     function getSongSeconds()
  158.         if type(song) == "table" then return song.length / song.tempo end
  159.     end
  160.    
  161.     -- Returns the song position in seconds.
  162.     function getSongPositionSeconds()
  163.         if type(song) == "table" then return cTick / song.tempo end
  164.     end
  165.    
  166.     -- Returns the tempo the song will be played at.
  167.     function getSongTempo()
  168.         if type(song) == "table" then return song.tempo end
  169.     end
  170.    
  171.     -- Switches to a different playback device.
  172.     function setPeripheral(...)
  173.         local newironnote = {}
  174.        
  175.         for i=1,#arg do if type(arg[i]) == "string" and peripheral.getType(arg[i]) == "iron_note" then
  176.             newironnote[#newironnote+1] = peripheral.wrap(arg[i])
  177.         elseif type(arg[i]) == "table" and arg[i].playNote then
  178.             newironnote[#newironnote+1] = arg[i]
  179.         end end
  180.        
  181.         if #newironnote > 0 then
  182.             ironnote = newironnote
  183.             for i = 1, #ironnote do ironnote[i] = ironnote[i].playNote end
  184.             return true
  185.         else return false end
  186.     end
  187.    
  188.     -- Switch to a remote playback device.
  189.     function registerRemoteSpeaker(speakerID)
  190.         if type(speakerID) ~= "number" and type(speakerID) ~= "string" then error("note.registerRemoteSpeaker: Expected: string (host name) or number (system ID). Got: "..type(speakerID)) end
  191.         speakerID = (not tonumber(speakerID)) and (rednet.lookup("MoarPNoteSpeaker", speakerID) or speakerID) or tonumber(speakerID)
  192.         if type(speakerID) ~= "number" then error("note.registerRemoteSpeaker: Rednet lookup failure on host \""..speakerID.."\".") end
  193.        
  194.         rednet.send(speakerID, "Hello?", "MoarPNoteSpeaker")
  195.         local incoming = {rednet.receive(10)}
  196.         if incoming[1] then haveVolume = incoming[2] else error("note.registerRemoteSpeaker: No reply from system ID "..speakerID..".", 2) end
  197.         remote = speakerID
  198.     end
  199.    
  200.     -- Returns whether music is loaded for playback.
  201.     function isPlaying()
  202.         return type(song) == "table"
  203.     end
  204.    
  205.     -- Returns whether playback is paused.
  206.     function isPaused()
  207.         return paused
  208.     end
  209.    
  210.     -- Returns the name of the song.
  211.     function getSongName()
  212.         if type(song) == "table" then return song.name end
  213.     end
  214.    
  215.     -- Returns the name of NBS file author.
  216.     function getSongAuthor()
  217.         if type(song) == "table" then return song.author end
  218.     end
  219.    
  220.     -- Returns the name of song artist.
  221.     function getSongArtist()
  222.         if type(song) == "table" then return song.originalauthor end
  223.     end
  224.    
  225.     -- Returns the song's description.
  226.     function getSongDescription()
  227.         if type(song) == "table" then return song.description end
  228.     end
  229.    
  230.     -- Gets the current volume level (between 0 and 1, inclusive).
  231.     function getVolumeLevel()
  232.         return haveVolume and volume
  233.     end
  234.    
  235.     -- Sets the current volume level (between 0 and 1, inclusive).
  236.     function setVolumeLevel(newVolume)
  237.         assert(type(newVolume) == "number", "note.setVolumeLevel(): Invalid argument.")
  238.        
  239.         if newVolume > 1 then
  240.             volume = 1
  241.         elseif newVolume < 0 then
  242.             volume = 0
  243.         else volume = newVolume end
  244.        
  245.         if remote then rednet.send(remote, volume, "MoarPNoteSpeaker") end
  246.     end
  247.    
  248.     local function byte_lsb(handle)
  249.         return assert(handle.read(), "Note NBS loading error: Unexpected EOF (end of file).", 2)
  250.     end
  251.  
  252.     local function int16_lsb(handle)
  253.         return bit.bor(bit.blshift(byte_lsb(handle), 8), byte_lsb(handle))
  254.     end
  255.  
  256.     local function int16_msb(handle)
  257.         local x = int16_lsb(handle)
  258.         --# convert little-endian to big-endian
  259.         local y = 0
  260.         y = y + bit.blshift(bit.band(x, 0x00FF), 8)
  261.         y = y + bit.brshift(bit.band(x, 0xFF00), 8)
  262.         return y
  263.     end
  264.  
  265.     local function int32_lsb(handle)
  266.         return bit.bor(bit.blshift(int16_lsb(handle), 16), int16_lsb(handle))
  267.     end
  268.  
  269.     local function int32_msb(handle)
  270.         local x = int32_lsb(handle)
  271.         --# convert little-endian to big-endian
  272.         local y = 0
  273.         y = y + bit.blshift(bit.band(x, 0x000000FF), 24)
  274.         y = y + bit.brshift(bit.band(x, 0xFF000000), 24)
  275.         y = y + bit.blshift(bit.band(x, 0x0000FF00), 8)
  276.         y = y + bit.brshift(bit.band(x, 0x00FF0000), 8)
  277.         return y
  278.     end
  279.  
  280.     local function nbs_string(handle)
  281.         local str = ""
  282.         for i = 1, int32_msb(handle) do
  283.             str = str..string.char(byte_lsb(handle))
  284.         end
  285.         return str
  286.     end
  287.  
  288.     local function readNbs(path)
  289.         assert(fs.exists(path), "Note NBS loading error: File \""..path.."\" not found. Did you forget to specify the containing folder?", 0)
  290.         assert(not fs.isDir(path), "Note NBS loading error: Specified file \""..path.."\" is actually a folder.", 0)
  291.         local handle = fs.open(path, "rb")
  292.  
  293.         song = { notes = {}; }
  294.  
  295.         --# NBS format found on http://www.stuffbydavid.com/nbs
  296.         --# Part 1: Header
  297.         song.length = int16_msb(handle)
  298.         local layers = int16_msb(handle)
  299.         song.name = safeString(nbs_string(handle))
  300.         song.author = safeString(nbs_string(handle))
  301.         song.originalauthor = safeString(nbs_string(handle))
  302.         song.description = safeString(nbs_string(handle))
  303.         song.tempo = int16_msb(handle)/100
  304.        
  305.         byte_lsb(handle) --# auto-saving has been enabled (0 or 1)
  306.         byte_lsb(handle) --# The amount of minutes between each auto-save (if it has been enabled) (1-60)
  307.         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
  308.         int32_msb(handle) --# The amount of minutes spent on the project
  309.         int32_msb(handle) --# The amount of times the user has left clicked
  310.         int32_msb(handle) --# The amount of times the user has right clicked
  311.         int32_msb(handle) --# The amount of times the user have added a block
  312.         int32_msb(handle) --# The amount of times the user have removed a block
  313.         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)
  314.  
  315.         --# Part 2: Note Blocks
  316.         local maxPitch = 24
  317.         local notes = song.notes
  318.         local tick = -1
  319.         local jumps = 0
  320.         while true do
  321.             jumps = int16_msb(handle)
  322.             if jumps == 0 then
  323.                 break
  324.             end
  325.             tick = tick + jumps
  326.             local layer = -1
  327.             while true do
  328.                 jumps = int16_msb(handle)
  329.                 if jumps == 0 then
  330.                     break
  331.                 end
  332.                 layer = layer + jumps
  333.                 local inst = byte_lsb(handle)
  334.                 local key = byte_lsb(handle)
  335.                 --
  336.                 notes[tick] = notes[tick] or {}
  337.                 table.insert(notes[tick], {inst = translate[inst]; pitch = math.max((key-33)%maxPitch,0); volume = layer})
  338.             end
  339.         end
  340.        
  341.         --# Part 3: Layers
  342.             if note.getVolumeLevel() then
  343.             local volume = {}
  344.             for i = 0, layers - 1 do
  345.                 nbs_string(handle)
  346.                 volume[i] = byte_lsb(handle) / 100
  347.             end
  348.             for i = 0, tick do if notes[i] then for _,note in pairs(notes[i]) do note.volume = volume[note.volume] end end end
  349.         else for i = 0, tick do if notes[i] then for _,note in pairs(notes[i]) do note.volume = nil end end end end
  350.        
  351.         --# Part 4: Custom Instruments
  352.         --# Ignored at this time.
  353.        
  354.         handle.close()
  355.     end
  356.  
  357.     function songEngine(targetSong)
  358.         assert(remote or ironnote[1], "Note songEngine failure: No Iron Note Blocks assigned.", 0)
  359.        
  360.         local haveVolume, tTick, curPeripheral, delay, notes, endTime = getVolumeLevel() ~= nil, os.startTimer(0.1), 1
  361.        
  362.         if targetSong then os.queueEvent("musicPlay",targetSong) end
  363.  
  364.         while true do
  365.             local e = { os.pullEvent() }
  366.            
  367.             if e[1] == "timer" and e[2] == tTick and song and not paused then
  368.                 if notes[cTick] then
  369.                     if not remote then
  370.                         local curMaxNotes, nowPlaying = (song.tempo == 20 and math.floor(MAX_INSTRUMENTS_PER_NOTE_BLOCK/2) or MAX_INSTRUMENTS_PER_NOTE_BLOCK) * #ironnote, 0
  371.                         for _,note in pairs(notes[cTick]) do
  372.                             if haveVolume then ironnote[curPeripheral](note.inst, note.pitch) else ironnote[curPeripheral](note.inst, note.pitch) end
  373.                             curPeripheral = (curPeripheral == #ironnote) and 1 or (curPeripheral + 1)
  374.                             nowPlaying = nowPlaying + 1
  375.                             if nowPlaying == curMaxNotes then break end
  376.                         end
  377.                     else rednet.send(remote, notes[cTick], "MoarPNoteSpeaker") end
  378.                 end
  379.                
  380.                 cTick = cTick + 1
  381.                
  382.                 if cTick > song.length then
  383.                     song = nil
  384.                     notes = nil
  385.                     cTick = nil
  386.                     os.queueEvent("musicFinished")
  387.                 else tTick = os.startTimer(endTime - (delay * (song.length + 1 - cTick)) - os.clock()) end
  388.                
  389.             elseif e[1] == "musicPause" then
  390.                 paused = true
  391.             elseif e[1] == "musicResume" then
  392.                 paused = false
  393.                 endTime = os.clock() + (delay * (song.length + 1 - cTick))
  394.                 tTick = os.startTimer(delay)
  395.             elseif e[1] == "musicSkipTo" then
  396.                 cTick = e[2]
  397.                 endTime = os.clock() + (delay * (song.length + 1 - cTick))
  398.             elseif e[1] == "musicPlay" then
  399.                 readNbs(e[2])
  400.                 notes = song.notes
  401.                 cTick = 0
  402.                 tTick = os.startTimer(0.1)
  403.                 paused = false
  404.                 delay = (100 / song.tempo) / 100
  405.                 endTime = os.clock() + (delay * (song.length + 1))
  406.                 os.queueEvent("newTrack")
  407.             end
  408.         end
  409.     end
  410.    
  411.     function playSong(targetSong)
  412.         parallel.waitForAny(function () songEngine(targetSong) end, function () os.pullEvent("musicFinished") end)
  413.     end
  414. else
  415.     -- -----------------------
  416.     -- Run as jukebox:
  417.     -- ------------------------------------------------------------
  418.    
  419.     -- Ignore everything below this point if you're only interested
  420.     -- in the API, unless you want to see example usage.
  421.    
  422.     -- ------------------------------------------------------------
  423.    
  424.     sleep(0)  -- 'cause ComputerCraft is buggy.
  425.    
  426.     os.loadAPI(shell.getRunningProgram())
  427.    
  428.     local startDir, playmode, lastSong, marqueePos, blackText, myEvent, bump, marquee, xSize, ySize = shell.resolve("."), 0, {}, 1, _CC_VERSION and colours.grey or colours.black
  429.     local playInitials = {{"R", 8}, {"N", 6}, {"M", 5}}
  430.     local cursor = {{">>  ","  <<"},{"> > "," < <"},{" >> "," << "},{"> > "," < <"}}
  431.     local logo = {{"| |","|\\|","| |"},{"+-+","| |","+-+"},{"---"," | "," | "},{"+--","|- ","+--"}}
  432.     local buttons = {{{"| /|","|< |","| \\|"},{"|\\ |","| >|","|/ |"},{"| |","| |","| |"},{"|\\ ","| >","|/ "}},  {{"|/|","|\\|"},{"|\\|","|/|"},{"| |","| |"},{"|\\ ","|/ "}}}
  433.  
  434.     -- Returns whether a click was performed at a given location.
  435.     -- If one parameter is passed, it checks to see if y is [1].
  436.     -- If two parameters are passed, it checks to see if x is [1] and y is [2].
  437.     -- If three parameters are passed, it checks to see if x is between [1]/[2] (non-inclusive) and y is [3].
  438.     -- If four paramaters are passed, it checks to see if x is between [1]/[2] and y is between [3]/[4] (non-inclusive).
  439.     local function clickedAt(...)
  440.         if myEvent[1] ~= "mouse_click" then return false end
  441.         if #arg == 1 then return (arg[1] == myEvent[4])
  442.         elseif #arg == 2 then return (myEvent[3] == arg[1] and myEvent[4] == arg[2])
  443.         elseif #arg == 3 then return (myEvent[3] > arg[1] and myEvent[3] < arg[2] and myEvent[4] == arg[3])
  444.         else return (myEvent[3] > arg[1] and myEvent[3] < arg[2] and myEvent[4] > arg[3] and myEvent[4] < arg[4]) end
  445.     end
  446.  
  447.     -- Returns whether one of a given set of keys was pressed.
  448.     local function pressedKey(...)
  449.         if myEvent[1] ~= "key" then return false end
  450.         for i=1,#arg do if arg[i] == myEvent[2] then return true end end
  451.         return false
  452.     end
  453.  
  454.     -- Ensures the display is suitable for play.
  455.     local function enforceScreenSize()
  456.         term.setTextColor(colours.white)
  457.         term.setBackgroundColor(colours.black)
  458.        
  459.         while true do
  460.             xSize, ySize = term.getSize()
  461.             term.clear()
  462.            
  463.             if xSize < 26 or ySize < 7 then
  464.                 term.setCursorPos(1,1)
  465.                 print("Display too small!\n")
  466.                 local myEvent = {os.pullEvent()}
  467.                 if myEvent[1] == "mouse_click" or myEvent[1] == "key" then error() end
  468.             else return end
  469.         end
  470.     end
  471.  
  472.     local function drawPlaymode()
  473.         term.setBackgroundColour(term.isColour() and colours.lightGrey or colours.black)
  474.         term.setTextColour(term.isColour() and colours.black or colours.white)
  475.        
  476.         if xSize > 49 then
  477.             term.setCursorPos(bump+34, 1)
  478.             term.write("[R]epeat    ( )")
  479.             term.setCursorPos(bump+34, 2)
  480.             term.write("Auto-[N]ext ( )")
  481.             term.setCursorPos(bump+34, 3)
  482.             term.write("[M]ix       ( )")
  483.  
  484.             if playmode ~= 0 then
  485.                 term.setTextColour(term.isColour() and colours.blue or colours.white)
  486.                 term.setCursorPos(bump+47, playmode)
  487.                 term.write("O")
  488.             end
  489.         else
  490.             term.setCursorPos(xSize - 12, 1)
  491.             term.write("   [R]epeat")
  492.             term.setCursorPos(xSize - 12, 2)
  493.             term.write("Auto-[N]ext")
  494.             term.setCursorPos(xSize - 12, 3)
  495.             term.write("      [M]ix")
  496.            
  497.             if playmode ~= 0 then
  498.                 term.setTextColour(term.isColour() and colours.blue or colours.black)
  499.                 if not term.isColour() then term.setBackgroundColour(colours.white) end
  500.                 term.setCursorPos(xSize - playInitials[playmode][2], playmode)
  501.                 term.write(playInitials[playmode][1])
  502.             end
  503.         end
  504.     end
  505.    
  506.     local function drawVolumeBar()
  507.         term.setCursorPos(9,ySize-2)
  508.         term.setBackgroundColour(term.isColour() and colours.brown or colours.black)
  509.         term.setTextColour(term.isColour() and colours.red or colours.white)
  510.         term.write("-")
  511.        
  512.         term.setBackgroundColour(term.isColour() and colours.white or colours.black)
  513.         term.setTextColour(term.isColour() and colours.lightGrey or blackText)
  514.         term.write(string.rep("V", xSize - 18))
  515.        
  516.         term.setBackgroundColour(term.isColour() and colours.green or colours.black)
  517.         term.setTextColour(term.isColour() and colours.lime or colours.white)
  518.         term.write("+")
  519.        
  520.         term.setTextColour(term.isColour() and colours.blue or colours.white)
  521.         term.setBackgroundColour(colours.black)
  522.         term.setCursorPos(10+(xSize-19)*note.getVolumeLevel(),ySize-2)
  523.         term.write("O")
  524.     end
  525.    
  526.     local function drawInterface()
  527.         if term.isColour() then
  528.             -- Header / footer.
  529.             term.setBackgroundColour(colours.grey)
  530.             for i = 1, 3 do
  531.                 term.setCursorPos(1,i)
  532.                 term.clearLine()
  533.                 term.setCursorPos(1,ySize-i+1)
  534.                 term.clearLine()
  535.             end
  536.            
  537.             -- Quit button.
  538.             term.setTextColour(colours.white)
  539.             term.setBackgroundColour(colours.red)
  540.             term.setCursorPos(xSize,1)
  541.             term.write("X")
  542.         end
  543.  
  544.         if xSize > 49 then
  545.             -- Note logo.
  546.             term.setTextColour(term.isColour() and colours.blue or colours.white)
  547.             term.setBackgroundColour(term.isColour() and colours.lightGrey or colours.black)
  548.             for i = 1, 4 do for j = 1, 3 do
  549.                 term.setCursorPos(bump + (i - 1) * 4, j)
  550.                 term.write(logo[i][j])
  551.             end end
  552.  
  553.             -- Skip back / forward buttons.
  554.             term.setTextColour(term.isColour() and colours.lightBlue or colours.white)
  555.             term.setBackgroundColour(term.isColour() and colours.grey or colours.black)
  556.             for j = 0, 1 do for i = 1, 3 do
  557.                 term.setCursorPos(bump + 17 + j * 11, i)
  558.                 term.write(buttons[1][j+1][i])
  559.             end end
  560.         else
  561.             -- Note logo.
  562.             term.setTextColour(term.isColour() and colours.blue or colours.white)
  563.             term.setBackgroundColour(term.isColour() and colours.lightGrey or colours.black)
  564.             term.setCursorPos(3, 1)
  565.             term.write(" N O T E ")
  566.    
  567.             -- Skip back / forward buttons.
  568.             term.setTextColour(term.isColour() and colours.lightBlue or colours.white)
  569.             term.setBackgroundColour(term.isColour() and colours.grey or colours.black)
  570.             for j = 0, 1 do for i = 1, 2 do
  571.                 term.setCursorPos(2 + j * 8, i + 1)
  572.                 term.write(buttons[2][j+1][i])
  573.             end end
  574.         end
  575.        
  576.         -- Progress bar.
  577.         term.setCursorPos(2,ySize-1)
  578.         term.setTextColour(term.isColour() and colours.black or colours.white)
  579.         term.setBackgroundColour(term.isColour() and colours.lightGrey or colours.black)
  580.         term.write("|"..string.rep("=",xSize-4).."|")
  581.        
  582.         drawPlaymode()
  583.         if note.getVolumeLevel() then drawVolumeBar() end
  584.     end
  585.    
  586.     local function startSong(newSong)
  587.         if #lastSong == 32 then lastSong[32] = nil end
  588.         table.insert(lastSong,1,newSong)
  589.         os.queueEvent("musicPlay",newSong)
  590.         marquee = nil
  591.         marqueePos = 1
  592.     end
  593.    
  594.     local function noteMenu()
  595.         local lastPauseState, dragX = "maybe"
  596.         bump = math.floor((xSize - 49) / 2) + 1
  597.         drawInterface()
  598.        
  599.         while true do
  600.             local displayList, position, lastPosition, animationTimer, curCount, gapTimer, lastProgress = {}, 1, 0, os.startTimer(0), 1
  601.             if #shell.resolve(".") > 0 then displayList[1] = ".." end
  602.  
  603.             do
  604.                 local fullList = fs.list(shell.resolve("."))
  605.                 table.sort(fullList, function (a, b) return string.lower(a) < string.lower(b) end)
  606.                 for i = 1, #fullList do if fs.isDir(shell.resolve(fullList[i])) then displayList[#displayList + 1] = fullList[i] end end
  607.                 for i = 1, #fullList do if fullList[i]:sub(#fullList[i]-3):lower() == ".nbs" then displayList[#displayList + 1] = fs.getName(fullList[i]) end end
  608.             end
  609.  
  610.             while true do
  611.                 myEvent = {os.pullEvent()}
  612.                
  613.                 if myEvent[1] == "mouse_click" then dragX = (myEvent[4] == ySize) and myEvent[3] or nil end
  614.                
  615.                 -- Track animations (bouncing, function (a, b) return string.lower(a) < string.lower(b) end cursor + scrolling marquee).
  616.                 if myEvent[1] == "timer" and myEvent[2] == animationTimer then
  617.                     if marquee then marqueePos = marqueePos == #marquee and 1 or (marqueePos + 1) end
  618.                     curCount = curCount == 4 and 1 or (curCount + 1)
  619.                     animationTimer = os.startTimer(0.5)
  620.                     myEvent[1] = "cabbage"
  621.                    
  622.                 -- Queue a new song to start playing, based on the playmode toggles (or if the user clicked the skip-ahead button).
  623.                 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
  624.                     if playmode == 1 then
  625.                         os.queueEvent("musicPlay",lastSong[1])
  626.                     elseif (playmode == 2 or (playmode == 0 and myEvent[1] ~= "timer")) and not fs.isDir(shell.resolve(displayList[#displayList])) then
  627.                         if shell.resolve(displayList[position]) == lastSong[1] or fs.isDir(shell.resolve(displayList[position])) then repeat
  628.                             position = position + 1
  629.                             if position > #displayList then position = 1 end
  630.                         until not fs.isDir(shell.resolve(displayList[position])) end
  631.                        
  632.                         startSong(shell.resolve(displayList[position]))
  633.                     elseif playmode == 3 and not fs.isDir(shell.resolve(displayList[#displayList])) then
  634.                         repeat position = math.random(#displayList) until not fs.isDir(shell.resolve(displayList[position]))
  635.                         startSong(shell.resolve(displayList[position]))
  636.                     end
  637.                    
  638.                     gapTimer = nil
  639.                     myEvent[1] = "cabbage"
  640.                
  641.                 elseif myEvent[1] ~= "timer" then   -- Special consideration, bearing in mind that the songEngine is spamming ten such events a second...
  642.                     -- Move down the list.
  643.                     if pressedKey(keys.down,keys.s) or (myEvent[1] == "mouse_scroll" and myEvent[2] == 1) then
  644.                         position = position == #displayList and 1 or (position + 1)
  645.  
  646.                     -- Move up the list.
  647.                     elseif pressedKey(keys.up,keys.w) or (myEvent[1] == "mouse_scroll" and myEvent[2] == -1) then
  648.                         position = position == 1 and #displayList or (position - 1)
  649.  
  650.                     -- Start a new song.
  651.                     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
  652.                         if fs.isDir(shell.resolve(displayList[position])) then
  653.                             shell.setDir(shell.resolve(displayList[position]))
  654.                             break
  655.                         else startSong(shell.resolve(displayList[position])) end
  656.  
  657.                     -- User clicked somewhere on the file list; move that entry to the currently-selected position.
  658.                     elseif clickedAt(0, xSize + 1, 3, ySize - 2) then
  659.                         position = position + myEvent[4] - math.floor(ySize / 2) - 1
  660.                         position = position > #displayList and #displayList or position
  661.                         position = position < 1 and 1 or position
  662.  
  663.                     -- Respond to a screen-resize; triggers a full display redraw.
  664.                     elseif myEvent[1] == "term_resize" or myEvent[1] == "monitor_resize" then
  665.                         enforceScreenSize()
  666.                         bump = math.floor((xSize - 49) / 2) + 1
  667.                         lastPosition = 0
  668.                         drawInterface()
  669.                         animationTimer = os.startTimer(0)
  670.                         lastPauseState = "maybe"
  671.  
  672.                     -- Quit.
  673.                     elseif pressedKey(keys.q, keys.x, keys.t) or clickedAt(xSize, 1) then
  674.                         if myEvent[1] == "key" then os.pullEvent("char") end
  675.                         os.unloadAPI("note")
  676.                         term.setTextColour(colours.white)
  677.                         term.setBackgroundColour(colours.black)
  678.                         term.clear()
  679.                         term.setCursorPos(1,1)
  680.                         print("Thanks for using the Note NBS player!\n")
  681.                         shell.setDir(startDir)
  682.                         error()
  683.  
  684.                     -- Toggle repeat mode.
  685.                     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
  686.                         playmode = playmode == 1 and 0 or 1
  687.                         drawPlaymode()
  688.  
  689.                     -- Toggle auto-next mode.
  690.                     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
  691.                         playmode = playmode == 2 and 0 or 2
  692.                         drawPlaymode()
  693.  
  694.                     -- Toggle mix (shuffle) mode.
  695.                     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
  696.                         playmode = playmode == 3 and 0 or 3
  697.                         drawPlaymode()
  698.  
  699.                     -- Music finished; wait a second or two before responding.
  700.                     elseif myEvent[1] == "musicFinished" then
  701.                         gapTimer = os.startTimer(2)
  702.                         lastPauseState = "maybe"
  703.                         marquee = ""
  704.  
  705.                     -- Skip back to start of the song (or to the previous song, if the current song just started).
  706.                     elseif pressedKey(keys.a,keys.left) or (xSize > 49 and clickedAt(bump+16,bump+21,0,4) or clickedAt(1,5,1,4)) then
  707.                         if note.isPlaying() and note.getSongPositionSeconds() > 3 then
  708.                             os.queueEvent("musicSkipTo",0)
  709.                             os.queueEvent("musicResume")
  710.                         elseif #lastSong > 1 then
  711.                             table.remove(lastSong,1)
  712.                             startSong(table.remove(lastSong,1))
  713.                         end
  714.  
  715.                     -- Toggle pause/resume.
  716.                     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
  717.                         if note.isPaused() then os.queueEvent("musicResume") else os.queueEvent("musicPause") end
  718.  
  719.                     -- Tracking bar clicked.
  720.                     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
  721.                         os.queueEvent("musicSkipTo",math.floor(note.getSongLength()*(myEvent[3]-1)/(xSize-2)))
  722.                    
  723.                     -- Song engine just initiated a new track.
  724.                     elseif myEvent[1] == "newTrack" then
  725.                         marquee = " [Title: "
  726.                         if note.getSongName() ~= "" then marquee = marquee..note.getSongName().."]" else marquee = marquee..fs.getName(lastSong[1]).."]" end
  727.                         if note.getSongArtist() ~= "" then marquee = marquee.." [Artist: "..note.getSongArtist().."]" end
  728.                         if note.getSongAuthor() ~= "" then marquee = marquee.." [NBS Author: "..note.getSongAuthor().."]" end
  729.                         marquee = marquee.." [Tempo: "..note.getSongTempo().."]"
  730.                         if note.getSongDescription() ~= "" then marquee = marquee.." [Description: "..note.getSongDescription().."]" end
  731.                         lastPauseState = "maybe"
  732.                    
  733.                     -- Drag the marquee.
  734.                     elseif myEvent[1] == "mouse_drag" and myEvent[4] == ySize and dragX and marquee then
  735.                         marqueePos = (marqueePos - myEvent[3] + dragX)%#marquee
  736.                         dragX = myEvent[3]
  737.                    
  738.                     elseif note.getVolumeLevel() then
  739.                         -- Volume down.
  740.                         if pressedKey(keys.minus,keys.underscore,keys.numPadSubtract) or clickedAt(9,ySize-2) then
  741.                             note.setVolumeLevel(note.getVolumeLevel()-0.05)
  742.                             drawVolumeBar()
  743.  
  744.                         -- Volume up.
  745.                         elseif pressedKey(keys.plus,keys.equals,keys.numPadAdd) or clickedAt(xSize-8,ySize-2) then
  746.                             note.setVolumeLevel(note.getVolumeLevel()+0.05)
  747.                             drawVolumeBar()
  748.  
  749.                         -- Volume bar clicked.
  750.                         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
  751.                             note.setVolumeLevel((myEvent[3]-10)/(xSize-19))
  752.                             drawVolumeBar()
  753.                         end
  754.                     end
  755.                 end
  756.                
  757.                 -- Play / pause button.
  758.                 if lastPauseState ~= note.isPaused() then
  759.                     term.setTextColour(term.isColour() and colours.lightBlue or colours.white)
  760.                     term.setBackgroundColour(term.isColour() and colours.grey or colours.black)
  761.                     if xSize > 49 then
  762.                         for i=1,3 do
  763.                             term.setCursorPos(bump + 23,i)
  764.                             term.write(buttons[1][(note.isPlaying() and not note.isPaused()) and 3 or 4][i])
  765.                         end
  766.                     else
  767.                         for i=1,2 do
  768.                             term.setCursorPos(6,i+1)
  769.                             term.write(buttons[2][(note.isPlaying() and not note.isPaused()) and 3 or 4][i])
  770.                         end
  771.                     end
  772.                     lastPauseState = note.isPaused()
  773.                 end
  774.                
  775.                 -- Update other screen stuff.
  776.                 if myEvent[1] ~= "timer" then
  777.                     term.setTextColour(term.isColour() and colours.black or colours.white)
  778.                     term.setBackgroundColour(term.isColour() and colours.lightGrey or colours.black)
  779.                    
  780.                     -- Clear old progress bar position.
  781.                     if lastProgress then
  782.                         term.setCursorPos(lastProgress,ySize-1)
  783.                         term.write((lastProgress == 2 or lastProgress == xSize - 1) and "|" or "=")
  784.                         lastProgress = nil
  785.                     end
  786.  
  787.                     -- Song timers.
  788.                     if note.isPlaying() then
  789.                         term.setCursorPos(xSize-5,ySize-2)
  790.                        
  791.                         local mins = tostring(math.min(99,math.floor(note.getSongSeconds()/60)))
  792.                         local secs = tostring(math.floor(note.getSongSeconds()%60))
  793.                         term.write((#mins > 1 and "" or "0")..mins..":"..(#secs > 1 and "" or "0")..secs)
  794.  
  795.                         term.setCursorPos(2,ySize-2)
  796.                         if note.isPaused() and bit.band(curCount,1) == 1 then
  797.                             term.write("     ")
  798.                         else
  799.                             mins = tostring(math.min(99,math.floor(note.getSongPositionSeconds()/60)))
  800.                             secs = tostring(math.floor(note.getSongPositionSeconds()%60))
  801.                             term.write((#mins > 1 and "" or "0")..mins..":"..(#secs > 1 and "" or "0")..secs)
  802.                         end
  803.  
  804.                         -- Progress bar position.
  805.                         term.setTextColour(term.isColour() and colours.blue or colours.white)
  806.                         term.setBackgroundColour(colours.black)
  807.                         lastProgress = 2+math.floor(((xSize-3) * note.getSongPosition() / note.getSongLength()))
  808.                         term.setCursorPos(lastProgress,ySize-1)
  809.                         term.write("O")
  810.                     else
  811.                         term.setCursorPos(2,ySize-2)
  812.                         term.write("00:00")
  813.                         term.setCursorPos(xSize-5,ySize-2)
  814.                         term.write("00:00")
  815.                     end
  816.  
  817.                     -- Scrolling marquee.
  818.                     if marquee then
  819.                         term.setTextColour(term.isColour() and colours.black or colours.white)
  820.                         term.setBackgroundColour(term.isColour() and colours.grey or colours.black)
  821.                         term.setCursorPos(1,ySize)
  822.  
  823.                         if marquee == "" then
  824.                             term.clearLine()
  825.                             marquee = nil
  826.                         else
  827.                             local thisLine = marquee:sub(marqueePos,marqueePos+xSize-1)
  828.                             while #thisLine < xSize do thisLine = thisLine..marquee:sub(1,xSize-#thisLine) end
  829.                             term.write(thisLine)
  830.                         end
  831.                     end
  832.                    
  833.                     -- File list.
  834.                     term.setBackgroundColour(colours.black)
  835.                     for y = position == lastPosition and (math.floor(ySize / 2)+1) or 4, position == lastPosition and (math.floor(ySize / 2)+1) or (ySize - 3) do
  836.                         local thisLine = y + position - math.floor(ySize / 2) - 1
  837.  
  838.                         if displayList[thisLine] then
  839.                             local thisString = displayList[thisLine]
  840.                             thisString = fs.isDir(shell.resolve(thisString)) and "["..thisString.."]" or thisString:sub(1,#thisString-4)
  841.  
  842.                             if thisLine == position then
  843.                                 term.setCursorPos(math.floor((xSize - #thisString - 8) / 2)+1, y)
  844.                                 term.clearLine()
  845.                                 term.setTextColour(term.isColour() and colours.cyan or blackText)
  846.                                 term.write(cursor[curCount][1])
  847.                                 term.setTextColour(term.isColour() and colours.blue or colours.white)
  848.                                 term.write(thisString)
  849.                                 term.setTextColour(term.isColour() and colours.cyan or blackText)
  850.                                 term.write(cursor[curCount][2])
  851.                             else
  852.                                 term.setCursorPos(math.floor((xSize - #thisString) / 2)+1, y)
  853.                                 term.clearLine()
  854.  
  855.                                 if y == 4 or y == ySize - 3 then
  856.                                     term.setTextColour(blackText)
  857.                                 elseif y == 5 or y == ySize - 4 then
  858.                                     term.setTextColour(term.isColour() and colours.grey or blackText)
  859.                                 elseif y == 6 or y == ySize - 5 then
  860.                                     term.setTextColour(term.isColour() and colours.lightGrey or colours.white)
  861.                                 else term.setTextColour(colours.white) end
  862.  
  863.                                 term.write(thisString)
  864.                             end
  865.                         else
  866.                             term.setCursorPos(1,y)
  867.                             term.clearLine()
  868.                         end
  869.                     end
  870.  
  871.                     lastPosition = position
  872.                 end
  873.             end
  874.         end
  875.     end
  876.    
  877.     local function openRednet()
  878.         local modems = {peripheral.find("modem", function(name, object) object.name = name return true end)}
  879.         for i = 1, #modems do rednet.open(modems[i].name) end
  880.     end
  881.    
  882.     local function beBluetoothSpeaker()
  883.         openRednet()
  884.        
  885.         local myName = (not os.getComputerLabel()) and ("Speaker"..math.random(10000)) or os.getComputerLabel()
  886.         rednet.host("MoarPNoteSpeaker", myName)
  887.         print("Hosting remote speaker service as \"" .. myName .. "\".")
  888.        
  889.         local haveVolume, ironnote, curPeripheral, nowPlaying, tick = note.getVolumeLevel() ~= nil, {peripheral.find("iron_note")}, 1, 0
  890.         for i = 1, #ironnote do ironnote[i] = ironnote[i].playNote end
  891.        
  892.         local x, y = term.getCursorPos()
  893.         term.write(#ironnote .. " speaker(s) available.")
  894.        
  895.         while true do
  896.             while #ironnote == 0 do
  897.                 os.pullEvent("peripheral")
  898.                 ironnote = {peripheral.find("iron_note")}
  899.                 for i = 1, #ironnote do ironnote[i] = ironnote[i].playNote end
  900.                 term.setCursorPos(1, y)
  901.                 term.clearLine()
  902.                 term.write(#ironnote .. " speaker(s) available.")
  903.             end
  904.            
  905.             local myEvent = {os.pullEventRaw()}
  906.            
  907.             if myEvent[1] == "rednet_message" and myEvent[4] == "MoarPNoteSpeaker" then
  908.                 if type(myEvent[3]) == "table" then
  909.                     for _,note in pairs(myEvent[3]) do
  910.                         if nowPlaying == MAX_INSTRUMENTS_PER_NOTE_BLOCK * #ironnote then break end
  911.                         if haveVolume then ironnote[curPeripheral](note.inst, note.pitch) else ironnote[curPeripheral](note.inst, note.pitch) end
  912.                         curPeripheral = (curPeripheral == #ironnote) and 1 or (curPeripheral + 1)
  913.                         nowPlaying = nowPlaying + 1
  914.                     end
  915.                     if not tick then tick = os.startTimer(0.1) end
  916.                 elseif type(myEvent[3]) == "number" then
  917.                     note.setVolumeLevel(myEvent[3])
  918.                 else rednet.send(myEvent[2], note.getVolumeLevel()) end
  919.             elseif myEvent[1] == "peripheral_detach" or myEvent[1] == "peripheral" then
  920.                 ironnote = {peripheral.find("iron_note")}
  921.                 for i = 1, #ironnote do ironnote[i] = ironnote[i].playNote end
  922.                 if curPeripheral > #ironnote then curPeripheral = 1 end
  923.                 term.setCursorPos(1, y)
  924.                 term.clearLine()
  925.                 term.write(#ironnote .. " speaker(s) available.")
  926.             elseif myEvent[1] == "timer" and myEvent[2] == tick then
  927.                 nowPlaying = 0
  928.                 tick = nil
  929.             elseif myEvent[1] == "terminate" then
  930.                 rednet.unhost("MoarPNoteSpeaker", myName)
  931.                 print()
  932.                 error()
  933.             end
  934.         end
  935.     end
  936.    
  937.     local function listBluetoothSpeakers()
  938.         openRednet()
  939.            
  940.         local servers = {rednet.lookup("MoarPNoteSpeaker")}
  941.        
  942.         print("Available Note servers:")
  943.         if #servers == 0 then print("(None)") end
  944.         for i=1,#servers do print(servers[i]) end
  945.         error()
  946.     end
  947.    
  948.     local function pairBluetoothSpeaker(speakerID)
  949.         openRednet()
  950.         note.registerRemoteSpeaker(speakerID)
  951.     end
  952.    
  953.     do local args = {...}
  954.     for i=1,#args do if not args[i] then
  955.         break
  956.     elseif args[i]:lower() == "-r" then
  957.         playmode = 1
  958.     elseif args[i]:lower() == "-n" then
  959.         playmode = 2
  960.     elseif args[i]:lower() == "-m" then
  961.         playmode = 3
  962.     elseif fs.isDir(shell.resolve(args[i])) then
  963.         shell.setDir(shell.resolve(args[i]))
  964.     elseif fs.isDir(args[i]) then
  965.         shell.setDir(args[i])
  966.     elseif fs.exists(shell.resolve(args[i])) then
  967.         local filePath = shell.resolve(args[i])
  968.         shell.setDir(fs.getDir(filePath))
  969.         startSong(filePath)
  970.     elseif fs.exists(shell.resolve(args[i]..".nbs")) then
  971.         local filePath = shell.resolve(args[i]..".nbs")
  972.         shell.setDir(fs.getDir(filePath))
  973.         startSong(filePath)
  974.     elseif fs.exists(args[i]) then
  975.         shell.setDir(fs.getDir(args[i]))
  976.         startSong(args[i])
  977.     elseif fs.exists(args[i]..".nbs") then
  978.         shell.setDir(fs.getDir(args[i]))
  979.         startSong(args[i]..".nbs")
  980.     elseif args[i]:lower() == "-server" then
  981.         beBluetoothSpeaker()
  982.     elseif args[i]:lower() == "-list" then
  983.         listBluetoothSpeakers()
  984.     elseif args[i]:lower() == "-remote" then
  985.         pairBluetoothSpeaker(table.remove(args, i + 1))
  986.     end end end
  987.    
  988.     if playmode > 1 then os.queueEvent("musicFinished") end
  989.    
  990.     enforceScreenSize()
  991.     parallel.waitForAny(note.songEngine, noteMenu)
  992. end
Advertisement
Add Comment
Please, Sign In to add comment