Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- +---------------------+------------+---------------------+
- -- | | | |
- -- | | Note API | |
- -- | | | |
- -- +---------------------+------------+---------------------+
- -- Note Block Song format + conversion tools: David Norgren
- -- Iron Note Block + NBS loading & playback: TheOriginalBIT
- -- Music player interface & API structure: Bomb Bloke
- -- ----------------------------------------------------------
- -- Place Note Block Studio NBS files on your ComputerCraft computer,
- -- then play them back via a MoarPeripheral's Iron Note Block!
- -- http://moarperipherals.com/index.php?title=Note
- -- http://www.computercraft.info/forums2/index.php?/topic/19357-moarperipherals
- -- http://www.minecraftforum.net/topic/136749-minecraft-note-block-studio
- -- This script can be ran as any other, but it can *also* be loaded as an API!
- -- Doing so exposes the following functions:
- -- < note.playSong(fileName) >
- -- Simply plays the specified NBS file.
- -- < note.songEngine([fileName]) >
- -- Plays the optionally specified NBS file, but whether one is specified or not, does
- -- not return when complete - instead this is intended to run continuously as a background
- -- process. Launch it via the parallel API and run it alongside your own script!
- -- While the song engine function is active, it can be manipulated by queuing the
- -- following events:
- -- * musicPlay
- -- Add a filename in as a parameter to start playback, eg:
- -- os.queueEvent("musicPlay","mySong.nbs")
- -- * musicPause
- -- Halts playback.
- -- * musicResume
- -- Resumes playback.
- -- * musicSkipTo
- -- Add a song position in as a parameter to skip to that segment. Specify the time
- -- in tenths of a second; for example, to skip a minute in use 600.
- -- Additionally, whenever the song engine finishes a track, it will automatically
- -- throw a "musicFinished" event, or a "newTrack" event whenever a new song is loaded.
- -- **Remember!** The API cannot respond to these events until YOUR code yields!
- -- Telling it to load a new song or jump to a different position won't take effect
- -- until you pull an event or something!
- -- < note.setPeripheral(targetDevice1, [targetDevice2,] ...) >
- -- By default, upon loading the API attaches itself to any Iron Note Blocks it detects.
- -- Use this if you have specific note block(s) in mind, or wish to use different blocks
- -- at different times - perhaps mid-song! Returns true if at least one of the specified
- -- devices was valid, or false if none were.
- -- **Note!** The Iron Note Block peripheral can currently play up to five instruments
- -- at any given moment. Providing multiple blocks to the API will cause it to
- -- automatically spread the load for those songs that need the extra notes.
- -- If you provide insufficient blocks, expect some notes to be skipped from
- -- certain songs.
- -- Very few songs (if any) require more than two Iron Note Blocks.
- -- < note.isPlaying() >
- -- Returns whether the API is currently mid-tune (ignoring whether it's paused or not).
- -- < note.isPaused() >
- -- Returns whether playback is paused.
- -- < note.getSongLength() >
- -- Returns the song length in "redstone updates". There are ten updates per second, or
- -- one per two game ticks.
- -- < note.getSongPosition() >
- -- Returns the song position in "redstone updates". Time in game ticks is 2 * this. Time
- -- in seconds is this / 10.
- -- < note.getSongSeconds() >
- -- Returns the song length in seconds.
- -- < note.getSongPositionSeconds() >
- -- Returns the song position in seconds.
- -- < note.getSongTempo() >
- -- Returns the song tempo, representing the "beats per second".
- -- Eg: 2.5 = one beat per 0.4 seconds.
- -- 5 = one beat per 0.2 seconds.
- -- 10 = one beat per 0.1 seconds.
- -- ... or whatever the song happens to be set to.
- -- "Should" be a factor of ten, but lots of NBS files have other tempos.
- -- < note.getSongName() >
- -- Returns the name of the song.
- -- < note.getSongAuthor() >
- -- Returns the name of the NBS file author.
- -- < note.getSongArtist() >
- -- Returns the name of the song artist.
- -- < note.getSongDescription() >
- -- Returns the song's description.
- --getvol, setvol, registerRemoteSpeaker
- -- ----------------------------------------------------------
- -- Cranking this value too high will cause crashes:
- local MAX_INSTRUMENTS_PER_NOTE_BLOCK = 5
- local CCver
- do
- local vers = {{_HOST, tonumber(_HOST and _HOST:match("%d+.%d+"))}, {_CC_VERSION, tonumber(_CC_VERSION)},
- {textutils.serializeJSON, 1.7}, {paintutils.drawBox, 1.64}, {fs.getDir, 1.63},
- {term.current, 1.6}}
- for i = 1, #vers do if vers[i][1] then
- CCver = vers[i][2]
- break
- end end
- if not CCver then error("Sorry, CC 1.6 or later is required.") end
- end
- if not shell then
- -- -----------------------
- -- Load as API:
- -- -----------------------
- local volume, paused, cTick, song, remote, haveVolume = 1
- local translate = {[0]=0,4,1,2,3}
- local noteName = {[0]="harp", "basedrum", "snare", "hat", "bass"}
- local noteName2 = {[0]="base", "basedrum", "snare", "hat", "bass"}
- -- Returns functions for each found "note block" attached; these take instrument/pitch/volume as their parameters.
- function getNoteblocks()
- local ironnote = {}
- haveVolume = false
- if peripheral.find("iron_note") then
- -- MoarPeripherals Iron Note Block
- local ironnote = {peripheral.find("iron_note")}
- for i = 1, #ironnote do
- local func = ironnote[i].playNote
- ironnote[i] = function(inst, pitch, vol) func(inst, pitch) end
- end
- elseif peripheral.find("speaker") then
- -- ComputerCraft Speaker
- ironnote = {peripheral.find("speaker")}
- haveVolume = true
- MAX_INSTRUMENTS_PER_NOTE_BLOCK = 20 -- Computronics crashes if spamming too many notes. Not sure what the safe limit is.
- for i = 1, #ironnote do
- local func = ironnote[i].playNote
- ironnote[i] = function(inst, pitch, vol) func(noteName2[inst], vol * 3, pitch) end
- end
- elseif peripheral.find("iron_noteblock") then
- -- Computronics Iron Note Block
- ironnote = {peripheral.find("iron_noteblock")}
- haveVolume = true
- MAX_INSTRUMENTS_PER_NOTE_BLOCK = 20 -- Beats me what the real limit is here but 20 should be plenty anyways.
- for i = 1, #ironnote do
- local func = ironnote[i].playNote
- ironnote[i] = function(inst, pitch, vol) func(inst, pitch, vol / 10) end -- NFI why volume works like this with Computronics.
- end
- elseif peripheral.find("neuralInterface", function(side, periph) return periph.hasModule("minecraft:noteblock") end)
- or peripheral.find("Music")
- or peripheral.find("minecraft:noteblock") then
- -- Plethora Note Block module
- ironnote = {peripheral.find("neuralInterface", function(side, periph) return periph.hasModule("minecraft:noteblock") end)}
- if #ironnote == 0 then ironnote = {peripheral.find("Music")} end
- if #ironnote == 0 then ironnote = {peripheral.find("minecraft:noteblock")} end
- haveVolume = true
- MAX_INSTRUMENTS_PER_NOTE_BLOCK = 20 -- Beats me what the real limit is here but 20 should be plenty anyways.
- for i = 1, #ironnote do
- local play = ironnote[i].playNote
- ironnote[i] = function(inst, pitch, vol) if vol >= 0.1 then coroutine.resume(coroutine.create(play), inst, pitch, vol) end end -- Don't ask
- end
- elseif peripheral.find("note_block") then
- -- OpenPeripherals Note Block
- haveVolume = true
- MAX_INSTRUMENTS_PER_NOTE_BLOCK = 20
- 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}
- ironnote = {peripheral.find("note_block")}
- 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"}
- for i = 1, #ironnote do
- local play = ironnote[i].playSound
- ironnote[i] = function(inst, pitch, vol) if vol >= 0.1 then play(comSound[inst], comPitch[pitch] or 0, vol) end end
- end
- elseif commands then
- -- Command Computer synth
- haveVolume = true
- MAX_INSTRUMENTS_PER_NOTE_BLOCK = 20
- local comPart = {}
- 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
- 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}
- if _HOST and not _HOST:find("Minecraft 1.8") then -- MC 1.9 or higher
- local comSound = {[0] = "block.note.harp", "block.note.basedrum", "block.note.snare", "block.note.hat", "block.note.bass"}
- ironnote = {function(inst, pitch, vol)
- commands.execAsync("playsound " .. comSound[inst] .. " block @a ~ ~ ~ " .. tostring(vol) .. " " .. tostring(comPitch[pitch]))
- commands.execAsync("particle note ~ ~1 ~ " .. tostring(comPart[pitch]) .. " 0 0 1")
- end}
- else
- local comSound = {[0] = "note.harp", "note.bd", "note.snare", "note.hat", "note.bass"}
- ironnote = {function(inst, pitch, vol)
- commands.execAsync("playsound " .. comSound[inst] .. " @a ~ ~ ~ " .. tostring(vol) .. " " .. tostring(comPitch[pitch]))
- commands.execAsync("particle note ~ ~1 ~ " .. tostring(comPart[pitch]) .. " 0 0 1")
- end}
- end
- end
- return unpack(ironnote)
- end
- local ironnote = {getNoteblocks()}
- local function assert(cdn, msg, lvl)
- if not cdn then
- error(msg or "assertion failed!", (lvl == 0 and 0 or lvl and (lvl + 1) or 2))
- end
- return cdn
- end
- -- Returns a string ComputerCraft can render.
- local safeString = CCver < 1.76 and
- function(text)
- local newText = {}
- for i = 1, #text do
- local val = text:byte(i)
- newText[i] = (val > 31 and val < 127) and val or 63
- end
- return string.char(unpack(newText))
- end
- or function(text) return text end
- -- Returns the song length.
- function getSongLength()
- if type(song) == "table" then return song.length end
- end
- -- Returns the song position.
- function getSongPosition()
- return cTick
- end
- -- Returns the song length in seconds.
- function getSongSeconds()
- if type(song) == "table" then return song.length / song.tempo end
- end
- -- Returns the song position in seconds.
- function getSongPositionSeconds()
- if type(song) == "table" then return cTick / song.tempo end
- end
- -- Returns the tempo the song will be played at.
- function getSongTempo()
- if type(song) == "table" then return song.tempo end
- end
- -- Switches to a different playback device.
- function setPeripheral(...)
- local newironnote = {}
- for i=1,#arg do if type(arg[i]) == "string" and peripheral.getType(arg[i]) == "iron_note" then
- newironnote[#newironnote+1] = peripheral.wrap(arg[i])
- elseif type(arg[i]) == "table" and arg[i].playNote then
- newironnote[#newironnote+1] = arg[i]
- end end
- if #newironnote > 0 then
- ironnote = newironnote
- for i = 1, #ironnote do ironnote[i] = ironnote[i].playNote end
- return true
- else return false end
- end
- -- Switch to a remote playback device.
- function registerRemoteSpeaker(speakerID)
- if type(speakerID) ~= "number" and type(speakerID) ~= "string" then error("note.registerRemoteSpeaker: Expected: string (host name) or number (system ID). Got: "..type(speakerID)) end
- speakerID = (not tonumber(speakerID)) and (rednet.lookup("MoarPNoteSpeaker", speakerID) or speakerID) or tonumber(speakerID)
- if type(speakerID) ~= "number" then error("note.registerRemoteSpeaker: Rednet lookup failure on host \""..speakerID.."\".") end
- rednet.send(speakerID, "Hello?", "MoarPNoteSpeaker")
- local incoming = {rednet.receive(10)}
- if incoming[1] then haveVolume = incoming[2] else error("note.registerRemoteSpeaker: No reply from system ID "..speakerID..".", 2) end
- remote = speakerID
- end
- -- Returns whether music is loaded for playback.
- function isPlaying()
- return type(song) == "table"
- end
- -- Returns whether playback is paused.
- function isPaused()
- return paused
- end
- -- Returns the name of the song.
- function getSongName()
- if type(song) == "table" then return song.name end
- end
- -- Returns the name of NBS file author.
- function getSongAuthor()
- if type(song) == "table" then return song.author end
- end
- -- Returns the name of song artist.
- function getSongArtist()
- if type(song) == "table" then return song.originalauthor end
- end
- -- Returns the song's description.
- function getSongDescription()
- if type(song) == "table" then return song.description end
- end
- -- Gets the current volume level (between 0 and 1, inclusive).
- function getVolumeLevel()
- return haveVolume and volume
- end
- -- Sets the current volume level (between 0 and 1, inclusive).
- function setVolumeLevel(newVolume)
- assert(type(newVolume) == "number", "note.setVolumeLevel(): Invalid argument.")
- if newVolume > 1 then
- volume = 1
- elseif newVolume < 0 then
- volume = 0
- else volume = newVolume end
- if remote then rednet.send(remote, volume, "MoarPNoteSpeaker") end
- end
- local function byte_lsb(handle)
- return assert(handle.read(), "Note NBS loading error: Unexpected EOF (end of file).", 2)
- end
- local function int16_lsb(handle)
- return bit.bor(bit.blshift(byte_lsb(handle), 8), byte_lsb(handle))
- end
- local function int16_msb(handle)
- local x = int16_lsb(handle)
- --# convert little-endian to big-endian
- local y = 0
- y = y + bit.blshift(bit.band(x, 0x00FF), 8)
- y = y + bit.brshift(bit.band(x, 0xFF00), 8)
- return y
- end
- local function int32_lsb(handle)
- return bit.bor(bit.blshift(int16_lsb(handle), 16), int16_lsb(handle))
- end
- local function int32_msb(handle)
- local x = int32_lsb(handle)
- --# convert little-endian to big-endian
- local y = 0
- y = y + bit.blshift(bit.band(x, 0x000000FF), 24)
- y = y + bit.brshift(bit.band(x, 0xFF000000), 24)
- y = y + bit.blshift(bit.band(x, 0x0000FF00), 8)
- y = y + bit.brshift(bit.band(x, 0x00FF0000), 8)
- return y
- end
- local function nbs_string(handle)
- local str = ""
- for i = 1, int32_msb(handle) do
- str = str..string.char(byte_lsb(handle))
- end
- return str
- end
- local function readNbs(path)
- assert(fs.exists(path), "Note NBS loading error: File \""..path.."\" not found. Did you forget to specify the containing folder?", 0)
- assert(not fs.isDir(path), "Note NBS loading error: Specified file \""..path.."\" is actually a folder.", 0)
- local handle = fs.open(path, "rb")
- local newSong = { notes = {}; }
- --# NBS format found on http://www.stuffbydavid.com/nbs
- --# Part 1: Header
- newSong.length = int16_msb(handle)
- local layers = int16_msb(handle)
- newSong.name = safeString(nbs_string(handle))
- newSong.author = safeString(nbs_string(handle))
- newSong.originalauthor = safeString(nbs_string(handle))
- newSong.description = safeString(nbs_string(handle))
- newSong.tempo = int16_msb(handle)/100
- byte_lsb(handle) --# auto-saving has been enabled (0 or 1)
- byte_lsb(handle) --# The amount of minutes between each auto-save (if it has been enabled) (1-60)
- 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
- int32_msb(handle) --# The amount of minutes spent on the project
- int32_msb(handle) --# The amount of times the user has left clicked
- int32_msb(handle) --# The amount of times the user has right clicked
- int32_msb(handle) --# The amount of times the user have added a block
- int32_msb(handle) --# The amount of times the user have removed a block
- 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)
- --# Part 2: Note Blocks
- local maxPitch = 24
- local notes = newSong.notes
- local tick = -1
- local jumps = 0
- while true do
- jumps = int16_msb(handle)
- if jumps == 0 then
- break
- end
- tick = tick + jumps
- local layer = -1
- while true do
- jumps = int16_msb(handle)
- if jumps == 0 then
- break
- end
- layer = layer + jumps
- local inst = byte_lsb(handle)
- local key = byte_lsb(handle)
- --
- notes[tick] = notes[tick] or {}
- table.insert(notes[tick], {inst = translate[inst]; pitch = math.max((key-33)%maxPitch,0); volume = layer})
- end
- end
- --# Part 3: Layers
- local volume = {}
- for i = 0, layers - 1 do
- nbs_string(handle)
- volume[i] = byte_lsb(handle) / 100
- end
- 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
- --# Part 4: Custom Instruments
- --# Ignored at this time.
- handle.close()
- song = newSong
- end
- function songEngine(targetSong)
- assert(remote or ironnote[1] or commands, "Note songEngine failure: No Iron Note Blocks assigned.", 0)
- local haveVolume, tTick, curPeripheral, delay, notes, endTime = getVolumeLevel() ~= nil, os.startTimer(0.1), 1
- if targetSong then os.queueEvent("musicPlay",targetSong) end
- while true do
- local e = { os.pullEvent() }
- if e[1] == "timer" and e[2] == tTick and song and not paused then
- if notes[cTick] then
- if not remote then
- local curMaxNotes, nowPlaying = (song.tempo == 20 and math.floor(MAX_INSTRUMENTS_PER_NOTE_BLOCK/2) or MAX_INSTRUMENTS_PER_NOTE_BLOCK) * #ironnote, 0
- for _,note in pairs(notes[cTick]) do
- ironnote[curPeripheral](note.inst, note.pitch, note.volume * volume)
- curPeripheral = (curPeripheral == #ironnote) and 1 or (curPeripheral + 1)
- nowPlaying = nowPlaying + 1
- if nowPlaying == curMaxNotes then break end
- end
- else rednet.send(remote, notes[cTick], "MoarPNoteSpeaker") end
- end
- cTick = cTick + 1
- if cTick > song.length then
- song = nil
- notes = nil
- cTick = nil
- os.queueEvent("musicFinished")
- else tTick = os.startTimer(endTime - (delay * (song.length + 1 - cTick)) - os.clock()) end
- elseif e[1] == "musicPause" then
- paused = true
- elseif e[1] == "musicResume" then
- paused = false
- endTime = os.clock() + (delay * (song.length + 1 - cTick))
- tTick = os.startTimer(delay)
- elseif e[1] == "musicSkipTo" then
- cTick = e[2]
- endTime = os.clock() + (delay * (song.length + 1 - cTick))
- elseif e[1] == "musicPlay" then
- readNbs(e[2])
- notes = song.notes
- cTick = 0
- tTick = os.startTimer(0.1)
- paused = false
- delay = (100 / song.tempo) / 100
- endTime = os.clock() + (delay * (song.length + 1))
- os.queueEvent("newTrack")
- end
- end
- end
- function playSong(targetSong)
- parallel.waitForAny(function () songEngine(targetSong) end, function () os.pullEvent("musicFinished") end)
- end
- else
- -- -----------------------
- -- Run as jukebox:
- -- ------------------------------------------------------------
- -- Ignore everything below this point if you're only interested
- -- in the API, unless you want to see example usage.
- -- ------------------------------------------------------------
- sleep(0) -- 'cause ComputerCraft is buggy.
- os.loadAPI(shell.getRunningProgram())
- local startDir, playmode, lastSong, marqueePos, blackText, myEvent, bump, marquee, xSize, ySize, logo, buttons, termBlit, dot = shell.resolve("."), 0, {}, 1
- local playInitials = {{"R", 8}, {"N", 6}, {"M", 5}}
- local cursor = {{">> "," <<"},{"> > "," < <"},{" >> "," << "},{"> > "," < <"}}
- if term.blit then
- termBlit = term.blit
- else
- local colourNum, hex = {}, "0123456789abcdef"
- for i = 1, 16 do colourNum[hex:sub(i, i)] = 2 ^ (i - 1) end
- termBlit = function(t, tC, bC)
- local curChars, curFG, curBG = {t:sub(1, 1)}, tC:sub(1, 1), bC:sub(1, 1)
- for i = 2, #t do
- local cT, cTC, cBC = t:sub(i, i), tC:sub(i, i), bC:sub(i, i)
- if cTC ~= curFG or cBC ~= curBG then
- term.setTextColour(colourNum[curFG])
- term.setBackgroundColour(colourNum[curBG])
- term.write(table.concat(curChars))
- curChars, curFG, curBG = {cT}, cTC, cBC
- else curChars[#curChars + 1] = cT end
- end
- term.setTextColour(colourNum[curFG])
- term.setBackgroundColour(colourNum[curBG])
- term.write(table.concat(curChars))
- end
- end
- if CCver >= 1.74 then -- ToDo: If adding support for older CC vers, check this for pre-1.55
- blackText = colours.grey
- else
- blackText = colours.black
- end
- if CCver >= 1.76 then
- logo = {{"\149\139\128\148\149\152\140\144\149\140\156\132\149\156\140\132",
- "\149\149\137\149\149\149\128\149\149\128\149\128\149\151\131\128",
- "\149\133\128\133\149\137\140\129\149\128\133\128\149\141\140\132"},
- {"788b7bbb7bbb7bbb", "7bbb7b8b78b87bb8", "7b8b7bbb78b87bbb"},
- {"8b88888888888888", "8888888888888888", "8888888888888888"}}
- buttons = {{{{"\151\128\135\148", "\149\145\128\149", "\138\128\139\133"},
- {"\151\139\128\148", "\149\128\157\149", "\138\135\128\133"},
- {"\151\128\148", "\149\128\149", "\138\128\133"},
- {"\151\144\128", "\149\128\132", "\138\129\128"}},
- {{"7773", "7733", "3733"}, {"7773", "7333", "3373"}, {"773", "773", "373"}, {"737", "733", "337"}},
- {{"3737", "3337", "7777"}, {"3377", "3377", "7777"}, {"377", "377", "777"}, {"377", "337", "777"}}},
- {{{"\149\135\149", "\149\139\149"}, {"\149\139\149", "\149\135\149"}, {"\149\128\149", "\149\128\149"}, {"\149\130\144", "\149\159\129"}},
- {{"773", "733"}, {"773", "733"}, {"773", "773"}, {"773", "733"}},
- {{"337", "377"}, {"337", "377"}, {"377", "377"}, {"337", "377"}}}}
- dot = "\007"
- else
- logo = {{" | | +-+ --- +--", " |\\| | | | |- ", " | | +-+ | +--"},
- {"bbbbbbbbbbbbbbbb", "bbbbbbbbbbbbbbbb", "bbbbbbbbbbbbbbbb"},
- {"7888788878887888", "7888788878887888", "7888788878887888"}}
- buttons = {{{{"| /|","|< |","| \\|"}, {"|\\ |","| >|","|/ |"}, {"| |","| |","| |"}, {"|\\ ","| >","|/ "}},
- {{"3333", "3333", "3333"}, {"3333", "3333", "3333"}, {"333", "333", "333"}, {"333", "333", "333"}},
- {{"7777", "7777", "7777"}, {"7777", "7777", "7777"}, {"777", "777", "777"}, {"777", "777", "777"}}},
- {{{"|/|","|\\|"}, {"|\\|","|/|"},{"| |","| |"}, {"|\\ ","|/ "}},
- {{"333", "333"}, {"333", "333"}, {"333", "333"}, {"333", "333"}},
- {{"777", "777"}, {"777", "777"}, {"777", "777"}, {"777", "777"}}}}
- dot = "O"
- end
- if not term.isColour() then
- local function monochrome(txt)
- local result = {}
- for i = 1, #txt do
- local char = txt:sub(i, i)
- 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")
- end
- return table.concat(result)
- end
- for i = 1, 3 do logo[2][i], logo[3][i] = monochrome(logo[2][i]), monochrome(logo[3][i]) end
- for i = 1, 2 do for j = 2, 3 do for k = 1, 4 do
- local thisTab = buttons[i][j][k]
- for l = 1, #thisTab do thisTab[l] = monochrome(thisTab[l]) end
- end end end
- end
- -- Returns whether a click was performed at a given location.
- -- If one parameter is passed, it checks to see if y is [1].
- -- If two parameters are passed, it checks to see if x is [1] and y is [2].
- -- If three parameters are passed, it checks to see if x is between [1]/[2] (non-inclusive) and y is [3].
- -- If four paramaters are passed, it checks to see if x is between [1]/[2] and y is between [3]/[4] (non-inclusive).
- local function clickedAt(...)
- if myEvent[1] ~= "mouse_click" then return false end
- if #arg == 1 then return (arg[1] == myEvent[4])
- elseif #arg == 2 then return (myEvent[3] == arg[1] and myEvent[4] == arg[2])
- elseif #arg == 3 then return (myEvent[3] > arg[1] and myEvent[3] < arg[2] and myEvent[4] == arg[3])
- else return (myEvent[3] > arg[1] and myEvent[3] < arg[2] and myEvent[4] > arg[3] and myEvent[4] < arg[4]) end
- end
- -- Returns whether one of a given set of keys was pressed.
- local function pressedKey(...)
- if myEvent[1] ~= "key" then return false end
- for i=1,#arg do if arg[i] == myEvent[2] then return true end end
- return false
- end
- -- Ensures the display is suitable for play.
- local function enforceScreenSize()
- term.setTextColor(colours.white)
- term.setBackgroundColor(colours.black)
- while true do
- xSize, ySize = term.getSize()
- term.clear()
- if xSize < 26 or ySize < 7 then
- term.setCursorPos(1,1)
- print("Display too small!\n")
- local myEvent = {os.pullEvent()}
- if myEvent[1] == "mouse_click" or myEvent[1] == "key" then error() end
- else return end
- end
- end
- local function drawPlaymode()
- term.setBackgroundColour(term.isColour() and colours.lightGrey or colours.black)
- term.setTextColour(term.isColour() and colours.black or colours.white)
- if xSize > 49 then
- term.setCursorPos(bump+34, 1)
- term.write("[R]epeat ( )")
- term.setCursorPos(bump+34, 2)
- term.write("Auto-[N]ext ( )")
- term.setCursorPos(bump+34, 3)
- term.write("[M]ix ( )")
- if playmode ~= 0 then
- term.setTextColour(term.isColour() and colours.blue or colours.white)
- term.setCursorPos(bump+47, playmode)
- term.write(dot)
- end
- else
- term.setCursorPos(xSize - 12, 1)
- term.write(" [R]epeat")
- term.setCursorPos(xSize - 12, 2)
- term.write("Auto-[N]ext")
- term.setCursorPos(xSize - 12, 3)
- term.write(" [M]ix")
- if playmode ~= 0 then
- term.setTextColour(term.isColour() and colours.blue or colours.black)
- if not term.isColour() then term.setBackgroundColour(colours.white) end
- term.setCursorPos(xSize - playInitials[playmode][2], playmode)
- term.write(playInitials[playmode][1])
- end
- end
- end
- local function drawVolumeBar()
- term.setCursorPos(9,ySize-2)
- term.setBackgroundColour(term.isColour() and colours.brown or colours.black)
- term.setTextColour(term.isColour() and colours.red or colours.white)
- term.write("-")
- term.setBackgroundColour(term.isColour() and colours.white or colours.black)
- term.setTextColour(term.isColour() and colours.lightGrey or blackText)
- term.write(string.rep("V", xSize - 18))
- term.setBackgroundColour(term.isColour() and colours.green or colours.black)
- term.setTextColour(term.isColour() and colours.lime or colours.white)
- term.write("+")
- term.setTextColour(term.isColour() and colours.blue or colours.white)
- term.setBackgroundColour(colours.black)
- term.setCursorPos(10+(xSize-19)*note.getVolumeLevel(),ySize-2)
- term.write(dot)
- end
- local function drawInterface()
- if term.isColour() then
- -- Header / footer.
- term.setBackgroundColour(colours.grey)
- for i = 1, 3 do
- term.setCursorPos(1,i)
- term.clearLine()
- term.setCursorPos(1,ySize-i+1)
- term.clearLine()
- end
- -- Quit button.
- term.setTextColour(colours.white)
- term.setBackgroundColour(colours.red)
- term.setCursorPos(xSize,1)
- term.write("X")
- end
- if xSize > 49 then
- -- Note logo.
- local t, tC, bC = logo[1], logo[2], logo[3]
- for i = 1, 3 do
- term.setCursorPos(1, i)
- termBlit(t[i], tC[i], bC[i])
- end
- -- Skip back / forward buttons.
- t, tC, bC = buttons[1][1], buttons[1][2], buttons[1][3]
- for j = 0, 1 do for i = 1, 3 do
- term.setCursorPos(bump + 17 + j * 11, i)
- termBlit(t[j+1][i], tC[j+1][i], bC[j+1][i])
- end end
- else
- -- Note logo.
- term.setTextColour(term.isColour() and colours.blue or colours.white)
- term.setBackgroundColour(term.isColour() and colours.lightGrey or colours.black)
- term.setCursorPos(3, 1)
- term.write(" N O T E ")
- -- Skip back / forward buttons.
- local t, tC, bC = buttons[2][1], buttons[2][2], buttons[2][3]
- for j = 0, 1 do for i = 1, 2 do
- term.setCursorPos(2 + j * 8, i + 1)
- termBlit(t[j+1][i], tC[j+1][i], bC[j+1][i])
- end end
- end
- -- Progress bar.
- term.setCursorPos(2,ySize-1)
- term.setTextColour(term.isColour() and colours.black or colours.white)
- term.setBackgroundColour(term.isColour() and colours.lightGrey or colours.black)
- term.write("|"..string.rep("=",xSize-4).."|")
- drawPlaymode()
- if note.getVolumeLevel() then drawVolumeBar() end
- end
- local function startSong(newSong)
- if #lastSong == 32 then lastSong[32] = nil end
- table.insert(lastSong,1,newSong)
- os.queueEvent("musicPlay",newSong)
- marquee = nil
- marqueePos = 1
- end
- local function noteMenu()
- local lastPauseState, dragX = "maybe"
- bump = math.floor((xSize - 49) / 2) + 1
- drawInterface()
- while true do
- local displayList, position, lastPosition, animationTimer, curCount, gapTimer, lastProgress = {}, 1, 0, os.startTimer(0), 1
- if #shell.resolve(".") > 0 then displayList[1] = ".." end
- do
- local fullList = fs.list(shell.resolve("."))
- table.sort(fullList, function (a, b) return string.lower(a) < string.lower(b) end)
- for i = 1, #fullList do if fs.isDir(shell.resolve(fullList[i])) then displayList[#displayList + 1] = fullList[i] end end
- for i = 1, #fullList do if fullList[i]:sub(#fullList[i]-3):lower() == ".nbs" then displayList[#displayList + 1] = fs.getName(fullList[i]) end end
- end
- while true do
- myEvent = {os.pullEvent()}
- if myEvent[1] == "mouse_click" then dragX = (myEvent[4] == ySize) and myEvent[3] or nil end
- -- Track animations (bouncing, function (a, b) return string.lower(a) < string.lower(b) end cursor + scrolling marquee).
- if myEvent[1] == "timer" and myEvent[2] == animationTimer then
- if marquee then marqueePos = marqueePos == #marquee and 1 or (marqueePos + 1) end
- curCount = curCount == 4 and 1 or (curCount + 1)
- animationTimer = os.startTimer(0.5)
- myEvent[1] = "cabbage"
- -- Queue a new song to start playing, based on the playmode toggles (or if the user clicked the skip-ahead button).
- 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
- if playmode == 1 then
- os.queueEvent("musicPlay",lastSong[1])
- elseif (playmode == 2 or (playmode == 0 and myEvent[1] ~= "timer")) and not fs.isDir(shell.resolve(displayList[#displayList])) then
- if shell.resolve(displayList[position]) == lastSong[1] or fs.isDir(shell.resolve(displayList[position])) then repeat
- position = position + 1
- if position > #displayList then position = 1 end
- until not fs.isDir(shell.resolve(displayList[position])) end
- startSong(shell.resolve(displayList[position]))
- elseif playmode == 3 and not fs.isDir(shell.resolve(displayList[#displayList])) then
- repeat position = math.random(#displayList) until not fs.isDir(shell.resolve(displayList[position]))
- startSong(shell.resolve(displayList[position]))
- end
- gapTimer = nil
- myEvent[1] = "cabbage"
- elseif myEvent[1] ~= "timer" then -- Special consideration, bearing in mind that the songEngine is spamming ten such events a second...
- -- Move down the list.
- if pressedKey(keys.down,keys.s) or (myEvent[1] == "mouse_scroll" and myEvent[2] == 1) then
- position = position == #displayList and 1 or (position + 1)
- -- Move up the list.
- elseif pressedKey(keys.up,keys.w) or (myEvent[1] == "mouse_scroll" and myEvent[2] == -1) then
- position = position == 1 and #displayList or (position - 1)
- -- Start a new song.
- 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
- if fs.isDir(shell.resolve(displayList[position])) then
- shell.setDir(shell.resolve(displayList[position]))
- break
- else startSong(shell.resolve(displayList[position])) end
- -- User clicked somewhere on the file list; move that entry to the currently-selected position.
- elseif clickedAt(0, xSize + 1, 3, ySize - 2) then
- position = position + myEvent[4] - math.floor(ySize / 2) - 1
- position = position > #displayList and #displayList or position
- position = position < 1 and 1 or position
- -- Respond to a screen-resize; triggers a full display redraw.
- elseif myEvent[1] == "term_resize" or myEvent[1] == "monitor_resize" then
- enforceScreenSize()
- bump = math.floor((xSize - 49) / 2) + 1
- lastPosition = 0
- drawInterface()
- animationTimer = os.startTimer(0)
- lastPauseState = "maybe"
- -- Quit.
- elseif pressedKey(keys.q, keys.x, keys.t) or clickedAt(xSize, 1) then
- if myEvent[1] == "key" then os.pullEvent("char") end
- os.unloadAPI("note")
- term.setTextColour(colours.white)
- term.setBackgroundColour(colours.black)
- term.clear()
- term.setCursorPos(1,1)
- print("Thanks for using the Note NBS player!\n")
- shell.setDir(startDir)
- error()
- -- Toggle repeat mode.
- 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
- playmode = playmode == 1 and 0 or 1
- drawPlaymode()
- -- Toggle auto-next mode.
- 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
- playmode = playmode == 2 and 0 or 2
- drawPlaymode()
- -- Toggle mix (shuffle) mode.
- 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
- playmode = playmode == 3 and 0 or 3
- drawPlaymode()
- -- Music finished; wait a second or two before responding.
- elseif myEvent[1] == "musicFinished" then
- gapTimer = os.startTimer(2)
- lastPauseState = "maybe"
- marquee = ""
- -- Skip back to start of the song (or to the previous song, if the current song just started).
- elseif pressedKey(keys.a,keys.left) or (xSize > 49 and clickedAt(bump+16,bump+21,0,4) or clickedAt(1,5,1,4)) then
- if note.isPlaying() and note.getSongPositionSeconds() > 3 then
- os.queueEvent("musicSkipTo",0)
- os.queueEvent("musicResume")
- elseif #lastSong > 1 then
- table.remove(lastSong,1)
- startSong(table.remove(lastSong,1))
- end
- -- Toggle pause/resume.
- 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
- if note.isPaused() then os.queueEvent("musicResume") else os.queueEvent("musicPause") end
- -- Tracking bar clicked.
- 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
- os.queueEvent("musicSkipTo",math.floor(note.getSongLength()*(myEvent[3]-1)/(xSize-2)))
- -- Song engine just initiated a new track.
- elseif myEvent[1] == "newTrack" then
- marquee = " [Title: "
- if note.getSongName() ~= "" then marquee = marquee..note.getSongName().."]" else marquee = marquee..fs.getName(lastSong[1]).."]" end
- if note.getSongArtist() ~= "" then marquee = marquee.." [Artist: "..note.getSongArtist().."]" end
- if note.getSongAuthor() ~= "" then marquee = marquee.." [NBS Author: "..note.getSongAuthor().."]" end
- marquee = marquee.." [Tempo: "..note.getSongTempo().."]"
- if note.getSongDescription() ~= "" then marquee = marquee.." [Description: "..note.getSongDescription().."]" end
- lastPauseState = "maybe"
- -- Drag the marquee.
- elseif myEvent[1] == "mouse_drag" and myEvent[4] == ySize and dragX and marquee then
- marqueePos = (marqueePos - myEvent[3] + dragX)%#marquee
- dragX = myEvent[3]
- elseif note.getVolumeLevel() then
- -- Volume down.
- if pressedKey(keys.minus,keys.underscore,keys.numPadSubtract) or clickedAt(9,ySize-2) then
- note.setVolumeLevel(note.getVolumeLevel()-0.05)
- drawVolumeBar()
- -- Volume up.
- elseif pressedKey(keys.plus,keys.equals,keys.numPadAdd) or clickedAt(xSize-8,ySize-2) then
- note.setVolumeLevel(note.getVolumeLevel()+0.05)
- drawVolumeBar()
- -- Volume bar clicked.
- 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
- note.setVolumeLevel((myEvent[3]-10)/(xSize-19))
- drawVolumeBar()
- end
- end
- end
- -- Play / pause button.
- if lastPauseState ~= note.isPaused() then
- local ind = (note.isPlaying() and not note.isPaused()) and 3 or 4
- if xSize > 49 then
- local t, tC, bC = buttons[1][1][ind], buttons[1][2][ind], buttons[1][3][ind]
- for i=1,3 do
- term.setCursorPos(bump + 23,i)
- termBlit(t[i], tC[i], bC[i])
- end
- else
- local t, tC, bC = buttons[2][1][ind], buttons[2][2][ind], buttons[2][3][ind]
- for i=1,2 do
- term.setCursorPos(6,i+1)
- termBlit(t[i], tC[i], bC[i])
- end
- end
- lastPauseState = note.isPaused()
- end
- -- Update other screen stuff.
- if myEvent[1] ~= "timer" then
- term.setTextColour(term.isColour() and colours.black or colours.white)
- term.setBackgroundColour(term.isColour() and colours.lightGrey or colours.black)
- -- Clear old progress bar position.
- if lastProgress then
- term.setCursorPos(lastProgress,ySize-1)
- term.write((lastProgress == 2 or lastProgress == xSize - 1) and "|" or "=")
- lastProgress = nil
- end
- -- Song timers.
- if note.isPlaying() then
- term.setCursorPos(xSize-5,ySize-2)
- local mins = tostring(math.min(99,math.floor(note.getSongSeconds()/60)))
- local secs = tostring(math.floor(note.getSongSeconds()%60))
- term.write((#mins > 1 and "" or "0")..mins..":"..(#secs > 1 and "" or "0")..secs)
- term.setCursorPos(2,ySize-2)
- if note.isPaused() and bit.band(curCount,1) == 1 then
- term.write(" ")
- else
- mins = tostring(math.min(99,math.floor(note.getSongPositionSeconds()/60)))
- secs = tostring(math.floor(note.getSongPositionSeconds()%60))
- term.write((#mins > 1 and "" or "0")..mins..":"..(#secs > 1 and "" or "0")..secs)
- end
- -- Progress bar position.
- term.setTextColour(term.isColour() and colours.blue or colours.white)
- term.setBackgroundColour(colours.black)
- lastProgress = 2+math.floor(((xSize-3) * note.getSongPosition() / note.getSongLength()))
- term.setCursorPos(lastProgress,ySize-1)
- term.write(dot)
- else
- term.setCursorPos(2,ySize-2)
- term.write("00:00")
- term.setCursorPos(xSize-5,ySize-2)
- term.write("00:00")
- end
- -- Scrolling marquee.
- if marquee then
- term.setTextColour(term.isColour() and colours.black or colours.white)
- term.setBackgroundColour(term.isColour() and colours.grey or colours.black)
- term.setCursorPos(1,ySize)
- if marquee == "" then
- term.clearLine()
- marquee = nil
- else
- local thisLine = marquee:sub(marqueePos,marqueePos+xSize-1)
- while #thisLine < xSize do thisLine = thisLine..marquee:sub(1,xSize-#thisLine) end
- term.write(thisLine)
- end
- end
- -- File list.
- term.setBackgroundColour(colours.black)
- for y = position == lastPosition and (math.floor(ySize / 2)+1) or 4, position == lastPosition and (math.floor(ySize / 2)+1) or (ySize - 3) do
- local thisLine = y + position - math.floor(ySize / 2) - 1
- if displayList[thisLine] then
- local thisString = displayList[thisLine]
- thisString = fs.isDir(shell.resolve(thisString)) and "["..thisString.."]" or thisString:sub(1,#thisString-4)
- if thisLine == position then
- term.setCursorPos(math.floor((xSize - #thisString - 8) / 2)+1, y)
- term.clearLine()
- term.setTextColour(term.isColour() and colours.cyan or blackText)
- term.write(cursor[curCount][1])
- term.setTextColour(term.isColour() and colours.blue or colours.white)
- term.write(thisString)
- term.setTextColour(term.isColour() and colours.cyan or blackText)
- term.write(cursor[curCount][2])
- else
- term.setCursorPos(math.floor((xSize - #thisString) / 2)+1, y)
- term.clearLine()
- if y == 4 or y == ySize - 3 then
- term.setTextColour(blackText)
- elseif y == 5 or y == ySize - 4 then
- term.setTextColour(term.isColour() and colours.grey or blackText)
- elseif y == 6 or y == ySize - 5 then
- term.setTextColour(term.isColour() and colours.lightGrey or colours.white)
- else term.setTextColour(colours.white) end
- term.write(thisString)
- end
- else
- term.setCursorPos(1,y)
- term.clearLine()
- end
- end
- lastPosition = position
- end
- end
- end
- end
- local function beBluetoothSpeaker()
- peripheral.find("modem", rednet.open)
- local myName = (not os.getComputerLabel()) and ("Speaker"..math.random(10000)) or os.getComputerLabel()
- rednet.host("MoarPNoteSpeaker", myName)
- print("Hosting remote speaker service as \"" .. myName .. "\".")
- local haveVolume, ironnote, curPeripheral, nowPlaying, tick = note.getVolumeLevel() ~= nil, {note.getNoteblocks()}, 1, 0
- local x, y = term.getCursorPos()
- term.write(#ironnote .. " speaker(s) available.")
- while true do
- while #ironnote == 0 do
- os.pullEvent("peripheral")
- ironnote = {peripheral.find("iron_note")}
- for i = 1, #ironnote do ironnote[i] = ironnote[i].playNote end
- term.setCursorPos(1, y)
- term.clearLine()
- term.write(#ironnote .. " speaker(s) available.")
- end
- local myEvent = {os.pullEventRaw()}
- if myEvent[1] == "rednet_message" and myEvent[4] == "MoarPNoteSpeaker" then
- if type(myEvent[3]) == "table" then
- for _,note in pairs(myEvent[3]) do
- if nowPlaying == MAX_INSTRUMENTS_PER_NOTE_BLOCK * #ironnote then break end
- ironnote[curPeripheral](note.inst, note.pitch, note.volume)
- curPeripheral = (curPeripheral == #ironnote) and 1 or (curPeripheral + 1)
- nowPlaying = nowPlaying + 1
- end
- if not tick then tick = os.startTimer(0.1) end
- elseif type(myEvent[3]) == "number" then
- note.setVolumeLevel(myEvent[3])
- else rednet.send(myEvent[2], note.getVolumeLevel()) end
- elseif myEvent[1] == "peripheral_detach" or myEvent[1] == "peripheral" then
- ironnote = {peripheral.find("iron_note")}
- for i = 1, #ironnote do ironnote[i] = ironnote[i].playNote end
- if curPeripheral > #ironnote then curPeripheral = 1 end
- term.setCursorPos(1, y)
- term.clearLine()
- term.write(#ironnote .. " speaker(s) available.")
- elseif myEvent[1] == "timer" and myEvent[2] == tick then
- nowPlaying = 0
- tick = nil
- elseif myEvent[1] == "terminate" then
- rednet.unhost("MoarPNoteSpeaker", myName)
- print()
- error()
- end
- end
- end
- local function listBluetoothSpeakers()
- peripheral.find("modem", rednet.open)
- local servers = {rednet.lookup("MoarPNoteSpeaker")}
- print("Available Note servers:")
- if #servers == 0 then print("(None)") end
- for i=1,#servers do print(servers[i]) end
- error()
- end
- local function pairBluetoothSpeaker(speakerID)
- peripheral.find("modem", rednet.open)
- note.registerRemoteSpeaker(speakerID)
- end
- do local args = {...}
- for i=1,#args do if not args[i] then
- break
- elseif args[i]:lower() == "-r" then
- playmode = 1
- elseif args[i]:lower() == "-n" then
- playmode = 2
- elseif args[i]:lower() == "-m" then
- playmode = 3
- elseif fs.isDir(shell.resolve(args[i])) then
- shell.setDir(shell.resolve(args[i]))
- elseif fs.isDir(args[i]) then
- shell.setDir(args[i])
- elseif fs.exists(shell.resolve(args[i])) then
- local filePath = shell.resolve(args[i])
- shell.setDir(fs.getDir(filePath))
- startSong(filePath)
- elseif fs.exists(shell.resolve(args[i]..".nbs")) then
- local filePath = shell.resolve(args[i]..".nbs")
- shell.setDir(fs.getDir(filePath))
- startSong(filePath)
- elseif fs.exists(args[i]) then
- shell.setDir(fs.getDir(args[i]))
- startSong(args[i])
- elseif fs.exists(args[i]..".nbs") then
- shell.setDir(fs.getDir(args[i]))
- startSong(args[i]..".nbs")
- elseif args[i]:lower() == "-server" then
- beBluetoothSpeaker()
- elseif args[i]:lower() == "-list" then
- listBluetoothSpeakers()
- elseif args[i]:lower() == "-remote" then
- pairBluetoothSpeaker(table.remove(args, i + 1))
- end end end
- if playmode > 1 then os.queueEvent("musicFinished") end
- enforceScreenSize()
- return parallel.waitForAny(note.songEngine, noteMenu)
- end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement