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